Sökresultat för

C++26 Contracts

72 minuter i lästid
Jens Riboe
Jens Riboe
Senior/Expert Software Developer
C++26 Contracts

Hur många gånger har du skrivit en kommentar ovanför en funktion, med liknande innebörd som denna "// OBS! n måste vara >= 0"? Vi vet alla hur ofta den läses. Nästa steg brukar bli ett assert() — som åtminstone kontrollerar saken, ända tills någon gör ett release-bygge med -DNDEBUG och kontrollen tyst försvinner. I C++26 finns äntligen ett tredje alternativ: skriv antagandet i själva signaturen av funktionen, där kompilatorn ser det, där den anropande koden ser det, och där det går att slå på och av utan att röra en enda rad kod.

I den här artikeln går jag igenom pre(), post() och contract_assert() — hur de skrivs, var de får sitta och vilka fällor som väntar. Vi tar en sväng förbi Tony Hoare 1969 och Bertrand Meyers programspråk Eiffel för att se varifrån idén kommer, bygger en ringbuffert där alla invarianter står i koden istället för i huvudet på den som skrev klassen, och kommer fram till det verkligt smarta greppet: vad som ska hända när ett kontrakt bryts står inte i källkoden — det bestämmer du när du kompilerar. På vägen dit tittar vi på hur man skriver en egen violation handler, vilka flaggor GCC 16 vill ha, det obegripliga länkfelet du får om du glömmer en av dem, och — minst lika viktigt — när man inte ska använda kontrakt. All kod i artikeln är byggd och körd på riktigt, och finns på GitHub.


Vad är design by contract?

Tanken är lånad rakt av från affärsvärlden. När två parter skriver ett avtal står det svart på vitt vad var och en åtar sig, och vad var och en har rätt att förvänta sig tillbaka. Bryter någon mot sin del är det inte en olyckshändelse — det är ett avtalsbrott, och det är någons fel.

Överför det till en funktion, så faller tre begrepp ut nästan av sig själva:

  • Precondition — vad anroparen lovar. sqrt_int(n) kräver att n inte är negativt. Uppfyller du inte det, har du brutit avtalet.
  • Postcondition — vad funktionen lovar tillbaka, under förutsättning att anroparen höll sin del. sqrt_int(n) lovar att returnera ett icke-negativt tal.
  • Invariant — vad som gäller hela tiden däremellan. En ringbuffert innehåller aldrig fler element än den har platser.

Det centrala — och det som är lätt att missa — är att det här inte är felhantering. Det är ansvarsfördelning. Ett kontrakt säger inte "här kan det gå fel och då gör vi så här". Det säger "det här kan inte inträffa, och gör det ändå har någon skrivit fel kod".

Skillnaden mot undantag och std::expected är värd att stanna upp vid, för den avgör när du ska använda vilket. En fil som inte finns är inte ett kontraktsbrott — filsystemet är utanför din kontroll, och att filen saknas är ett fullt förväntat utfall som programmet ska hantera snyggt. Ett negativt tal till sqrt_int() är däremot ett kontraktsbrott: ingen korrekt kod kan någonsin göra det anropet. Kontrakt handlar om programmerarfel. Undantag och expected handlar om körningsfel.

Har du programmerat ett tag har du redan gjort det här, fast för hand. Kommentaren ovanför funktionen:

// OBS! n måste vara >= 0, annars odefinierat beteende
int sqrt_int(int n);

Det där är ett kontrakt. Problemet är bara att det är skrivet till en människa som inte läser det, kontrollerat av ingen, och osynligt för alla verktyg. Ett antagande som bara står i en kommentar är inte ett antagande — det är en förhoppning.

Nästa steg i den evolutionen brukar vara assert(), och det är ett riktigt steg framåt: nu kontrolleras det faktiskt. Men assert() har sina egna bekymmer, som vi återkommer till. Det som C++26 ger oss är samma idé, fast inbyggd i språket, synlig i signaturen, och med en av- och påslagsknapp som sitter i bygget istället för i koden.


Historien: från Hoare till C++26

Idén är betydligt äldre än C++. Den är faktiskt äldre än C.

1969 — Hoare och den axiomatiska grunden

År 1969 publicerade C.A.R. Hoare (Sir Charles Antony Richard Hoare) artikeln An Axiomatic Basis for Computer Programming. Där introducerade han det som numera kallas Hoare-trippeln:

{P} C {Q}

Läs det som: om påståendet P gäller innan programsatsen C körs, så gäller påståendet Q efteråt. P är preconditionen, Q är postconditionen, och C är koden däremellan. Hela poängen var att kunna resonera om programkorrekthet matematiskt istället för att testa sig fram.

Det är alltså exakt pre och post — formulerat 57 år innan C++ fick dem.

Få personer har påverkat hur vi programmerar på fundamental nivå, i samma grad som Tony Hoare. Förutom det här med pre & post, så uppfann han quicksort algoritmen, formulerade CSP (Communicating Sequential Processes) som resulterade i programspråket Occam för Transputers (1980-talets motsvarighet till Raspberry Pi) och otaliga andra språk, och monitor-begreppet som ligger till grund för användandet av mutex-lock och condition-variable i fler-trådade program och mer därtill. Ja, just det; han uppfann också värdet null för pekare och flera decennier senare stod han på en konferens 2009 och sa att detta var hans "billion-dollar mistake". Detta har man ju tagit till sig i Rust, där null saknas helt, till förmån för datatyper som Option<T> och Result<T,E>.

C.A.R. Hoare

1986 — Meyer och Eiffel

Hoares trippel var teori. Det var Bertrand Meyer som gjorde den till en programmeringsteknik. I samband med språket Eiffel (1986) myntade han begreppet Design by Contract och byggde in det direkt i syntaxen, med nyckelorden require, ensure och invariant.

Så här ser det ut i Eiffel:

class ACCOUNT

feature -- Access
    balance: INTEGER
            -- Current balance

feature -- Element change
    deposit (sum: INTEGER)
            -- Add `sum' to account.
        require
            non_negative: sum >= 0
        do
            balance := balance + sum
        ensure
            updated: balance = old balance + sum
        end

invariant
    non_negative_balance: balance >= 0

end -- class ACCOUNT

Känns det bekant? Det borde det göra. require är pre och ensure är post. Skillnaden i uttryckskraft är dock större än den ser ut vid första anblicken, och det är värt att notera två saker som Eiffel hade redan 1986 och som C++26 inte har:

  • old — i post-condition kan man referera till värdet som något hade vid inträdet. balance = old balance + sum säger att saldot ska ha ökat med exakt insättningen. Det går inte att uttrycka i C++26 — ännu.
  • Klassinvarianter — ett villkor som kontrolleras efter varje publik medlemsfunktion, automatiskt. Vi får se när detta kommer till C++.
Bertrand Meyer

Mellanspelet: assert() och alla hemmabyggen

C fick assert() i C89, och den har tjänat oss troget i snart fyrtio år. Jag har själv strött den över mer kod än jag vill kännas vid. Men den har fyra irriterande egenskaper:

  1. Den syns inte i signaturen. Den ligger inuti en funktion, dit anroparen aldrig tittar.
  2. Den har exakt två lägen: på eller av, via NDEBUG. Inget däremellan.
  3. Om villkoret är brutet, så avbryts programmet. Punkt. Du kan inte logga och fortsätta.
  4. Du kan inte styra vad som händer — ingen egen hanterare, ingen egen formatering.

Resultatet blev, som alltid när standarden inte räcker till, att varenda kodbas byggde sin egen. MY_ASSERT, VERIFY, CHECK, REQUIRE, ASSERT_MSG, och så Expects/Ensures från GSL för dem som läst Core Guidelines. Alla gör ungefär samma sak, ingen gör det likadant, och ingen av dem går att slå av och på utifrån.

2018–2019 — contracts i C++20, och ut igen

Contracts röstades faktiskt in i C++20. Och sedan ut igen.

På mötet i Köln i juli 2019 — feature freeze för C++20 — gjordes en sen och omfattande designändring, och förtroendet för hela förslaget rasade ihop. Resultatet blev P1823R0, med den föredömligt kortfattade titeln "Remove Contracts from C++20". Det är en av de mer dramatiska vändningarna i kommitténs historia.

Kärnfrågan man inte kunde enas om var enkel att ställa och kolossalt svår att besvara: vad ska hända när ett kontrakt bryts? Terminera? Kasta ett undantag? Logga och fortsätta? Olika läger hade olika — och förvisso helt legitima — behov. Ett inbyggt system vill terminera direkt. En testsvit vill logga allt och köra vidare. En release-build vill inte betala ett dugg.

Efter urröstningen bildades studiegruppen SG21 med ett enda uppdrag: gör om, gör rätt. Nästan sex år senare landade P2900, som röstades in i C++26.

Och lösningen på kärnfrågan? Man valde helt enkelt att inte välja. Vad som händer vid ett kontraktsbrott är inte en egenskap hos koden — det är en egenskap hos bygget. Det är den designen som gör denna språknyhet riktigt intressant.

Idén är för övrigt långt ifrån unik för C++. Ada har Pre/Post sedan Ada 2012, D har in/out/invariant, Kotlin har require/check, Clojure har :pre/:post, och .NET hade ett tag Code Contracts. C++ är snarare sent på bollen än tidigt.


Syntaxen: pre() och post()

Nog med historia. Så här ser det ut för en funktion som begränsar ett värde (v) till att ligga inom ett intervall (lo..hi):

int clamp(int v, int const lo, int const hi)
    pre(lo <= hi)
    post(r: lo <= r and r <= hi)
{
    return v < lo ? lo : (v > hi ? hi : v);
}

Kontrakten står mellan signaturen och kroppen — på samma plats som noexcept och override skulle ha stått. Det är ingen slump: det är just där de hör hemma, som en del av funktionens gränssnitt och inte av dess implementation.

Punkt för punkt:

  • pre(expr) kontrolleras vid inträdet i funktionen, innan kroppen körs.
  • post(expr) kontrolleras vid utträdet, efter att kroppen är klar.
  • post(r: expr) binder returvärdet till namnet r, så att du kan uttala dig om det. Och r är bara en identifierare — inget nyckelord. post(result: ...) eller post(x: ...) fungerar precis lika bra, vilket vi kommer att utnyttja senare.
  • Behöver du inte returvärdet skriver du bara post(expr) utan bindning, till exempel post(not empty()).
  • Du kan ha flera av varje. pre(a) pre(b) är fullt tillåtet och läser ofta bättre än pre(a and b), eftersom felmeddelandet då pekar ut exakt vilket villkor som brast.

Predikaten är helt vanlig C++. Det finns inget minispråk att lära sig — du kan anropa funktioner, använda operatorer, läsa medlemsvariabler. Enda kravet är att uttrycket går att konvertera till bool.

Fällan: const på by-value-parametrar

Här går man på en mina direkt, och det är värt att gå på den medvetet istället för i skarpt läge. Följande ser fullständigt rimligt ut:

int clamp(int v, int lo, int hi)
    pre(lo <= hi)
    post(r: lo <= r and r <= hi)
{
    return v < lo ? lo : (v > hi ? hi : v);
}

Men det kompilerar inte:

$ g++ -std=c++26 -fcontracts -c clamp.cxx
clamp.cxx:3:13: error: a value parameter used in a postcondition must be const
    3 |     post(r: lo <= r and r <= hi)
      |             ^~
clamp.cxx:1:22: note: parameter declared here
    1 | int clamp(int v, int lo, int hi)
      |                  ~~~~^~

Varför så sträng? För att funktionskroppen får ändra sina egna parameterkopior hur den vill. Skrev vi lo = 0; någonstans i kroppen skulle postconditionen plötsligt mäta mot ett annat lo än det anroparen skickade in — och då lovar funktionen något helt annat än vad signaturen påstår. const blockerar den möjligheten.

Lösningen är enkel: sätt const på de parametrar du nämner i din postcondition. Notera att v klarar sig utan, eftersom den inte förekommer där.

int clamp(int v, int const lo, int const hi)

Detta gäller bara by value. Parametrar som tas som const& är redan konstanta, och för dem är det här ett icke-problem.

Var kontrakt får sitta

Fria funktioner, medlemsfunktioner, konstruktorer — och, lite oväntat, lambdor:

auto half = [](int const n) pre(n % 2 == 0) post(r: 2 * r == n) { return n / 2; };

Det fungerar precis som man hoppas. Däremot finns en begränsning som är lätt att gå in i:

$ g++ -std=c++26 -fcontracts -c shape.cxx
shape.cxx:3:18: error: contracts cannot be added to virtual functions
    3 |     virtual auto area() const -> double post(r: r > 0.0);
      |                  ^~~~

Kontrakt på virtuella funktioner finns inte i C++26. Det är inte någon kompilatorbrist utan ett medvetet beslut. Motiveringen i P2899 är ovanligt rakt på sak om saken:

"The ability to specify precondition and postcondition assertions on virtual functions is important for adopting contract assertions into codebases that make use of runtime polymorphism. Nevertheless, [P2900R14] intentionally omits this feature. […] We expect that this feature will be added as a future extension to the Contracts proposal."

Det är alltså inte om, utan när. P3097R3 antogs in i utkastet till C++29 på Brno-mötet i juni 2026. Herb Sutters reserapport: "less than three months after C++26 was technically finalized, draft standard C++(29) contracts do support virtual functions."

Deklaration kontra definition

Ett kontrakt kan — och bör ofta — sitta på deklarationen i header-filen. Då blir det en del av det publicerade API:t, synligt för varenda anropare som inkluderar headern:

// int-sqrt.hxx
#pragma once

int int_sqrt(int const n)
    pre(n >= 0)
    post(r: r >= 0);

Regeln som gäller är att alla deklarationer av en funktion måste bära samma kontrakt. Lägger du till en post i .cxx-filen som headern inte har, blir det kompileringsfel:

error: declaration has a different number of contracts than previously declared
note: previous declaration with fewer contracts here

Du får antingen upprepa dem identiskt i definitionen, eller utelämna dem helt där. Båda varianterna är lagliga, och vi kommer strax till varför man kanske vill upprepa dem.


contract_assert() — invarianter mitt i koden

pre och post sitter på funktionens gräns. Men ibland vill man hävda något mitt inne i en kropp — en loopinvariant, ett antagande efter en beräkning. Då används contract_assert():

int sum_to(int const n)
    pre(n >= 0)
    post(r: r >= 0)
{
    auto total = 0;
    for (auto k = 1; k <= n; ++k) {
        total += k;
        contract_assert(total > 0);   // loop invariant
    }
    return total;
}

Det här är den standardiserade ersättaren för assert(). Men det viktiga är inte att den gör samma sak — det är att den gör det på samma villkor som pre och post. Samma byggflagga styr den, samma hanterare tar emot brottet, samma formatering på utskriften. Ett enda system istället för två.

assert() contract_assert()
Styrs av NDEBUG, av eller på Fyra utvärderingssemantiker
Vid brott Alltid abort() Konfigurerbart per bygge
Logga och fortsätta Nej Ja, med observe
Egen hanterare Nej Ja
Diagnostik i compile-time Ja, men som __assert_fail Ja, med predikatet i klartext
Standardiserad Ja, sedan C89 Ja, från C++26

Notera särskilt den näst sista raden. Vi kommer tillbaka till den, för den är roligare än den låter.


Demot: CircularBuffer<T, N>

Dags att sätta ihop delarna till något som liknar riktig kod. En ringbuffert är ett bra exempel, eftersom den är full av invarianter som är lätta att formulera i ord och lätta att slarva bort i koden.

Illustration of a ring buffer

Konstruktorn

template<typename ElemType, unsigned CAPACITY = 10U>
class CircularBuffer {
    std::array<ElemType, CAPACITY> buffer{};
    unsigned put_idx{}, get_idx{}, num_elems{};

public:
    CircularBuffer() pre(CAPACITY >= 3U) { }

Ett kontrakt på en template-parameter. En ringbuffert med plats för två element är knappast meningsfull, och istället för att skriva det i en kommentar — eller, ännu värre, låta bli — står det nu i koden. I just fallet med en template-parameter kan man förvisso använda en static_assert(), men då missar vi poängen i denna artikel.

put()

    void put(ElemType const& x)
        pre(put_idx < capacity() and not full())
        post(not empty())
    {
        buffer[put_idx] = x;
        ++put_idx %= capacity();
        ++num_elems;
        contract_assert(size() <= capacity());
    }

Tre kontrakt på några rader kod, och alla tre säger något som annars bara hade funnits i huvudet på den som skrev klassen:

  • Insättningsindexet ligger inom bufferten, och det finns plats kvar.
  • Efteråt är bufferten garanterat inte tom — vi har ju precis lagt in något.
  • Antalet element överskrider aldrig kapaciteten.

Lägg märke till att predikaten anropar klassens egna const-metoder. Kontrakt är vanlig C++, och full(), empty() och size() är precis de begrepp man ändå tänker i termer av.

get()

    auto get() -> ElemType
        pre(get_idx < capacity() and not empty())
        post(x: x == prev() and not full())
    {
        auto x = buffer[get_idx];
        ++get_idx %= capacity();
        --num_elems;
        contract_assert(size() < capacity());
        return x;
    }

Här blir det intressant. Postconditionen post(x: x == prev() and not full()) binder returvärdet till x och jämför det mot en privat hjälpmetod:

private:
    auto& prev() const {
        return buffer.at((get_idx + capacity() - 1) % capacity());
    }

prev() pekar ut elementet precis före det uppdaterade läsindexet — alltså det som nyss lämnades ut. Postconditionen verifierar därmed att vi faktiskt returnerade rätt element, inte bara ett element. Det är precis den sortens kontroll man vet att man borde skriva ett test för, och precis den sortens test man aldrig kommer till skott med.

Notera också att jag kallade bindningen x här och r i clamp(). Som sagt: det är bara ett namn.

Körningen

Huvudprogrammet gör tre saker: en normal genomkörning, en overflow, och ett försök att skapa en buffert som är för liten.

int main() {
    {
        std::println("--- Normal operation ---");
        auto buf = CircularBuffer<int, 5>{};
        for (auto k = 1; not buf.full(); ++k) buf.put(k);
        for (auto first = true; not buf.empty(); first = false)
            std::print("{}{}", (first ? "" : ", "), buf.get());
        std::println();
    }
    {
        std::println("--- Overflow: one put() too many ---");
        auto buf = CircularBuffer<int, 3>{};
        for (auto k = 1; not buf.full(); ++k) buf.put(k);
        buf.put(666);
    }
    {
        std::println("--- Invalid capacity: too small ---");
        auto _ = CircularBuffer<int, 1>{};
    }
    std::println("--- Done ---");
}

Byggt och kört med semantiken observe, som loggar brottet och låter programmet fortsätta:

$ g++ -std=c++26 -Wall -Wextra -fcontracts \
      -fcontract-evaluation-semantic=observe \
      src/circular-buffer.cxx -o circular-buffer
$ ./circular-buffer
--- Normal operation ---
1, 2, 3, 4, 5
--- Overflow: one put() too many ---
contract violation in function void CircularBuffer<ElemType, CAPACITY>::put(const ElemType&) [with ElemType = int; unsigned int CAPACITY = 3] at src/circular-buffer.cxx:16: put_idx < capacity() and not full()
[assertion_kind: pre, semantic: observe, mode: predicate_false, terminating: no]
contract violation in function void CircularBuffer<ElemType, CAPACITY>::put(const ElemType&) [with ElemType = int; unsigned int CAPACITY = 3] at src/circular-buffer.cxx:22: size() <= capacity()
[assertion_kind: assert, semantic: observe, mode: predicate_false, terminating: no]
--- Invalid capacity: too small ---
contract violation in function CircularBuffer<ElemType, CAPACITY>::CircularBuffer() [with ElemType = int; unsigned int CAPACITY = 1] at src/circular-buffer.cxx:13: CAPACITY >= 3U
[assertion_kind: pre, semantic: observe, mode: predicate_false, terminating: no]
--- Done ---

Titta noga på overflow-fallet. Det smäller två gånger:

  1. Först pre(put_idx < capacity() and not full()) — bufferten var full, anroparen bröt avtalet.
  2. Sedan contract_assert(size() <= capacity()) — eftersom observe betyder "logga och kör vidare" fortsatte put() att köra, num_elems räknades upp till 4, och nu rymmer en buffert med tre platser plötsligt fyra element.

Man ser buggen fortplanta sig. Kontrakt nummer ett visar var felet uppstod, kontrakt nummer två visar vad det ledde till. Det är precis det observe är till för, och det är något assert() aldrig kunnat ge dig — den hade tagit kål på processen vid punkt ett.


Fyra semantiker — valet görs vid bygget

Nu kommer vi till designbeslutet som fick hela förslaget att gå i lås. Vad som händer vid ett kontraktsbrott står inte i koden. Det bestäms när du kompilerar.

Semantik Utvärderas? Anropar hanteraren? Terminerar? Typiskt användningsfall
ignore Nej Nej Release. Noll overhead.
observe Ja Ja Nej Införande i befintlig kodbas, testkörningar
enforce Ja Ja Ja Debug- och CI-byggen. Default i GCC.
quick_enforce Ja Nej Ja Säkerhetskritiskt, minimal kodstorlek

Samma källkod, fyra beteenden. Här är samma ringbuffert byggd fyra gånger:

ignore — kontrakten kompileras bort helt:

$ ./circular-buffer-ignore
--- Normal operation ---
1, 2, 3, 4, 5
--- Overflow: one put() too many ---
--- Invalid capacity: too small ---
--- Done ---

observe — loggar och fortsätter, som vi såg ovan.

enforce — loggar och terminerar:

$ ./circular-buffer-enforce
--- Normal operation ---
1, 2, 3, 4, 5
--- Overflow: one put() too many ---
contract violation in function void CircularBuffer<ElemType, CAPACITY>::put(const ElemType&) [with ElemType = int; unsigned int CAPACITY = 3] at src/circular-buffer.cxx:16: put_idx < capacity() and not full()
[assertion_kind: pre, semantic: enforce, mode: predicate_false, terminating: yes]
terminate called without an active exception
Aborted (core dumped)
$ echo $?
134

quick_enforce — terminerar direkt, utan att ens anropa hanteraren:

$ ./circular-buffer-quick_enforce
--- Normal operation ---
1, 2, 3, 4, 5
--- Overflow: one put() too many ---
terminate called without an active exception
Aborted (core dumped)

Det är observe som är det verkliga nytillskottet. Den semantiken gör det praktiskt möjligt att dra på kontrakt över en befintlig kodbas utan att allt genast slutar fungera. Bygg med observe, kör testsviten, samla in loggen, och se hur illa det står till — innan du slår på enforce och börjar terminera processer.

Praktisk fälla. abort() flushar inte en buffrad stdout. Under enforce eller quick_enforce kan därför hela utskriften före kraschen försvinna, särskilt om du pipear output till en fil. Första gången jag såg det trodde jag att programmet dog direkt vid start. Demoprogrammen sätter därför radbuffring i början av main():

std::setvbuf(stdout, nullptr, _IOLBF, 0);

_IOLBF innebär radbuffring (I/O Line Buffering), det vill säga "töm efter varje \n".


Kompilatorstöd och byggflaggor

Läget hösten 2026 är att GCC 16 är det enda man kan arbeta med på riktigt. Jag har verifierat att både en egenbyggd gcc-16.1-release och Ubuntus trunk-snapshot g++-16 klarar allt i den här artikeln.

Det cirkulerar en del blogginlägg som påstår att man behöver bygga en särskild contracts-gren av GCC. Det stämde ett tag, men gäller inte längre — contracts ligger i mainline GCC 16. Clang har en experimentell gren på gång, MSVC har såvitt jag kunnat utröna ingenting ännu.

Grundkommandot ser ut så här:

$ g++ -std=c++26 -Wall -Wextra -Wpedantic \
      -fcontracts \
      -fcontract-evaluation-semantic=observe \
      src/circular-buffer.cxx -o circular-buffer

Flaggorna som finns:

Flagga Betydelse
-fcontracts Slår på hela finessen. Behövs även vid länkning.
-fcontract-evaluation-semantic= ignore | observe | enforce | quick_enforce. Default: enforce.
-fcontracts-client-check= none | pre | all. Kontrollera även hos anroparen. Default: none.
-fcontracts-definition-check= on | off. Kontrollera inuti funktionsdefinitionen. Default: on.

Väggen du kommer att gå in i

Av allt i den här artikeln är det här det mest sannolika sättet att fastna, och felmeddelandet hjälper inte ett dugg:

$ g++ -std=c++26 -fcontracts -fcontract-evaluation-semantic=observe \
      -c src/circular-buffer.cxx -o circular-buffer.o
$ g++ circular-buffer.o -o circular-buffer
/usr/bin/ld: circular-buffer.o: in function `__tu_has_violation(...)':
circular-buffer.cxx:(.text+0x266): undefined reference to
  `handle_contract_violation(std::contracts::contract_violation const&)'
collect2: error: ld returned 1 exit status

Inte ett ord om contracts. Bara en saknad symbol med ett namn som inte finns någonstans i din kod.

Förklaringen är att -fcontracts vid länkningen är det som drar in standardhanteraren. Utelämnar du den där, och inte har skrivit en egen hanterare, står länkaren utan handle_contract_violation. Lägg till flaggan i länksteget också, så går det:

$ g++ -fcontracts circular-buffer.o -o circular-buffer

I CMake blir det motsvarande:

target_compile_options(my-app PRIVATE
        -fcontracts
        -fcontract-evaluation-semantic=observe)
target_link_options(my-app PRIVATE -fcontracts)

Klientsideskontroll

Som standard kontrollerar GCC bara preconditionen inuti funktionsdefinitionen. Men minns det ursprungliga försäljningsargumentet: "ett brutet kontrakt rapporteras vid anropsstället". Det är vad -fcontracts-client-check=pre ger dig.

Ta ett litet bibliotek i två filer. Header-filen deklarerar kontraktet:

// src/client-check/int-sqrt.hxx
int int_sqrt(int const n)
    pre(n >= 0)
    post(r: r >= 0);

Och definitionen upprepar det:

// src/client-check/int-sqrt.cxx
int int_sqrt(int const n)
    pre(n >= 0)
    post(r: r >= 0)
{
    return static_cast<int>(std::sqrt(n));
}

Slutligen, i main() anropar vi med argumenten 49 respektive -4 och ser vad som händer:

std::println("int_sqrt(49) = {}", int_sqrt(49));
std::println("int_sqrt(-4) = {}", int_sqrt(-4));

Med -fcontracts-client-check=none (default) kontrolleras pre bara på ett ställe:

$ ./client-check-none
int_sqrt(49) = 7
contract violation in function int int_sqrt(int) at src/client-check/int-sqrt.cxx:10: n >= 0
[assertion_kind: pre, semantic: observe, mode: predicate_false, terminating: no]
contract violation in function int int_sqrt(int) at src/client-check/int-sqrt.cxx:11: r >= 0
[assertion_kind: post, semantic: observe, mode: predicate_false, terminating: no]
int_sqrt(-4) = -2147483648

Med =pre kontrolleras den två gånger — en gång hos anroparen och en gång i definitionen:

$ ./client-check-pre
int_sqrt(49) = 7
contract violation in function int int_sqrt(int) at src/client-check/int-sqrt.hxx:7: n >= 0
[assertion_kind: pre, semantic: observe, mode: predicate_false, terminating: no]
contract violation in function int int_sqrt(int) at src/client-check/int-sqrt.cxx:10: n >= 0
[assertion_kind: pre, semantic: observe, mode: predicate_false, terminating: no]
contract violation in function int int_sqrt(int) at src/client-check/int-sqrt.cxx:11: r >= 0
[assertion_kind: post, semantic: observe, mode: predicate_false, terminating: no]
int_sqrt(-4) = -2147483648

Det är just därför det kan löna sig att upprepa kontrakten i definitionen: filnamn och radnummer skiljer de två kontrollerna åt, så du ser direkt vilken som var vilken. Hade kontrakten bara stått i headern hade båda raderna pekat på headern.

Kontrakt vid kompileringstid

Det här är en cool detalj. Kontrakt fungerar i constexpr-sammanhang, och då får man statisk analys, gratis:

constexpr int fact(int const n)
    pre(n >= 0)
{
    return n <= 1 ? 1 : n * fact(n - 1);
}

int main() {
    constexpr auto bad = fact(-1);
    return bad;
}
$ g++ -std=c++26 -fcontracts -fcontract-evaluation-semantic=enforce fact.cxx
fact.cxx: In function 'int main()':
fact.cxx:2:5: error: contract predicate is false in constant expression
    2 |     pre(n >= 0)
      |     ^~~~~~~~~~~

Ett brutet kontrakt i ett konstantuttryck blir ett kompileringsfel under enforce, och en varning under observe. Programmet kommer aldrig ens i närheten av att köras.

En liten sak som kan spara någon en halvtimmes felsökning: feature-test-makrot __cpp_contracts är i GCC 16.1 definierat till 202502 även utan -fcontracts. Det duger alltså inte som villkor för att avgöra om finessen är påslagen.


Egen violation handler

Standardhanterarens utskrift är korrekt, men utskriften underlättar inte direkt en snabb analys av vad som har blivit fel:

contract violation in function void CircularBuffer<ElemType, CAPACITY>::put(const ElemType&) [with ElemType = int; unsigned int CAPACITY = 3] at src/circular-buffer.cxx:16: put_idx < capacity() and not full()
[assertion_kind: pre, semantic: observe, mode: predicate_false, terminating: no]

Hela template-instansieringen, absolut sökväg, och predikatet allra sist på raden. I ett riktigt projekt vill du antagligen ha det i ditt eget loggformat, med ditt eget request-id påklistrat, och kanske en räknare i din metrics-uppsättning.

Det går, och det är förbluffande enkelt. Deklarera en global funktion med exakt den här signaturen:

void handle_contract_violation(std::contracts::contract_violation const& v);

Det är allt. Ingen registrering, inget makro, ingen header som resten av programmet behöver inkludera. Finns symbolen vid länkningen väljs din hanterare, annars standardens.

Vad du får veta

Objektet std::contracts::contract_violation bär all information om brottet:

Egenskap Returtyp Beskrivning
kind() assertion_kind pre | post | assert
semantic() evaluation_semantic ignore | observe | enforce | quick_enforce
is_terminating() bool true om programmet kommer att avslutas
mode() detection_mode predicate_false | evaluation_exception
comment() const char* Predikatet som text
location() source_location Fil, rad och funktionsnamn

Två avvikelser att känna till. P2900 och cppreference kallar den fjärde egenskapen detection_mode(), men libstdc++ i GCC 16.1 stavar den mode(). Och även om detection_mode::evaluation_exception finns i headern, så rapporterar GCC 16.1 predicate_false även när predikatet kastar ett undantag. Jag har provat. Det är sådant som ger sig med tiden, men just nu får man dras med "skönhetsfelet".

En hanterare som gör jobbet

#include <print>
#include <string_view>
#include <contracts>

using namespace std::contracts;

namespace {
    constexpr auto to_text(assertion_kind k) -> std::string_view {
        switch (k) {
            case assertion_kind::pre:    return "pre";
            case assertion_kind::post:   return "post";
            case assertion_kind::assert: return "assert";
        }
        return "?";
    }

    // Drop the absolute project-folder prefix, keeping the path from "src/" on.
    constexpr auto strip_project_dir(std::string_view path) -> std::string_view {
        if (auto const pos = path.rfind("/src/"); pos != std::string_view::npos)
            return path.substr(pos + 1);
        return path;
    }
}

void handle_contract_violation(contract_violation const& v) {
    auto const loc = v.location();
    std::println("[{}] {}  ({}:{})",
                 to_text(v.kind()),
                 v.comment(),
                 strip_project_dir(loc.file_name()),
                 loc.line());
}

Samma brott som ovan, men nu betydligt mer läsvärt:

[pre] put_idx < capacity() and not full()  (src/circular-buffer.cxx:16)

Om du inte kan bestämma dig och vill ha både och, så kan du från din egen handler anropa standardens version via:

std::contracts::invoke_default_contract_violation_handler( v );

Vad hanteraren inte får göra

Här gäller det att hålla tungan rätt i mun. Din hanterare anropas när programmet redan befinner sig i ett trasigt tillstånd — något som skulle vara omöjligt har just visat sig vara möjligt. Det betyder:

  • Allokera inte. Är det heapen som är korrupt gör du bara saken värre.
  • Kasta inte undantag. Under enforce är du på väg mot terminate() ändå.
  • Anropa inte tillbaka in i koden som bröt kontraktet. Vägen till obegränsad rekursion är kort.
  • Håll den snabb. Under observe kan den anropas väldigt många gånger.

Och varför den inte ligger i ett bibliotek

En rimlig tanke är att paketera ihop hanteraren med sitt loggramverk och distribuera den som en del av ett bibliotek. Det fungerar dåligt, och anledningen är mekanismen själv: handle_contract_violation är en enda global symbol för hela programmet. Länkar du in två bibliotek som båda tagit med sin egen hanterare får du en duplicate symbol, och även om det går igenom är det inte längre applikationens eget val vilken som vinner.

Hanteraren hör därför hemma i applikationens sista länksteg — hos den som äger huvudprogrammet. Det är därför den ligger som en fristående .cxx-fil i demoprojektet och inte i en header. Ett bibliotek kan gott erbjuda en hanterare, men det ska vara applikationen som aktivt väljer att länka in den.


När ska man använda kontrakt — och när inte?

Som brukligt när man är mjukvaruingenjör så gäller det att veta när man ska applicera en idé och när så inte är fallet.

Använd kontrakt för:

  • API-antaganden som idag står i en kommentar eller ingenstans alls.
  • Interna invarianter — "det här kan aldrig inträffa".
  • Gradvis införande i en äldre kodbas, med observe som första steg.
  • Att ersätta hemmabyggda ASSERT-makron med något standardiserat.

Använd inte kontrakt för:

  • Validering av användarindata. Att någon skriver "abc" i ett åldersfält är inte ett programmerarfel.
  • Fel från filsystem, nätverk eller databaser. De är förväntade och ska hanteras.
  • Något som får hända i produktion. Bryts ett kontrakt är programmet trasigt — då är std::expected eller ett undantag (exception) rätt verktyg, inte ett kontrakt.

Tumregeln: kan en korrekt anropare någonsin utlösa villkoret, är det inte ett kontrakt.

Två praktiska råd på köpet. Håll predikaten enkla och fria från sidoeffekter — ett pre() som anropar en komplex funktion betalar du för vid varje anrop under observe och enforce. Och skriv hellre pre(a) pre(b) än pre(a and b), så pekar felmeddelandet ut exakt vilket villkor som brast.


Vad som inte kom med

Det som röstades in i C++26 är uttryckligen en MVPminimum viable product. Motiveringen i P2899 listar vad som medvetet lämnades utanför, och det är värt att känna till innan man blir besviken:

  • Kontrakt på virtuella funktioner. Planerat som framtida utvidgning.
  • old-värden i postconditions. Rationalet tar upp precis det exempel man först tänker på: postconditionen för std::vector::push_back — att storleken ska ha ökat med ett — går inte att uttrycka i C++26.
  • Klassinvarianter. Det som Eiffel hade 1986. Bedömdes ha för många öppna frågor för att hålla upp hela finessen.
  • Etiketter på kontrakt, för att välja semantik per enskilt kontrakt istället för per bygge.
  • Kontrakt på funktionspekare och typalias till sådana.

Det är, med andra ord, en första version. Men det är en genomtänkt första version, och — till skillnad från C++20-försöket — en som faktiskt gick i mål.

Specifikationen är för övrigt betydligt trevligare läsning än man kan tro, och väl värt en kvart om du undrar varför någon detalj ser ut som den gör. Det, och alla andra referenser i artikeln, hittar du under Länkar sist på sidan.

C++ standard committee voting for contracts

Sammanfattning

Vi har rest en rätt lång sträcka: från Hoares {P} C {Q} 1969, via Eiffels require och ensure, genom assert() och fyrtio år av hemmabyggda makron, förbi den pinsamma urröstningen ur C++20, och fram till pre, post och contract_assert i C++26.

Det som är nytt är inte idén. Idén är äldre än de flesta av oss. Det som är nytt är att antagandet äntligen står i signaturen istället för i kommentaren ovanför — där kompilatorn ser det, där anroparen ser det, och där det går att kontrollera.

Och det verkligt smarta greppet, det som fick förslaget att gå i mål den här gången, är att C++26 vägrar svara på frågan "vad ska hända vid ett kontraktsbrott?". Det svaret hör inte hemma i koden. ignore i release, observe när du inför kontrakt i en gammal kodbas, enforce i CI. Samma källkod, olika stränghet, inget att kommentera bort.

Är det produktionsklart? Tja, det beror på. Språkdelen fungerar förvånansvärt bra — jag har inte lyckats få GCC att göra något oväntat med själva semantiken — men runt omkring skaver det. Länkfelet utan förklaring, mode() som skulle hetat detection_mode(), evaluation_exception som aldrig rapporteras, feature-test-makrot som är påslaget även när finessen är avslagen. Inget av det är blockerande, men allt av det kostar dig en kvart första gången.

Mitt råd: klona demot — länken finns sist på sidan — bygg det med alla fyra semantikerna, och lek en stund. Ta sedan en klass i din egen kod — helst en med invarianter du redan har i huvudet men aldrig skrivit ner — och sätt pre och post på den. Bygg med observe och kör din testsvit.

Jag gissar att du blir förvånad över vad som ramlar ut.