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 attninte ä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 tillsqrt_int()är däremot ett kontraktsbrott: ingen korrekt kod kan någonsin göra det anropet. Kontrakt handlar om programmerarfel. Undantag ochexpectedhandlar 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ärdetnullfö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ärnullsaknas helt, till förmån för datatyper somOption<T>ochResult<T,E>.
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 + sumsä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++.
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:
- Den syns inte i signaturen. Den ligger inuti en funktion, dit anroparen aldrig tittar.
- Den har exakt två lägen: på eller av, via
NDEBUG. Inget däremellan. - Om villkoret är brutet, så avbryts programmet. Punkt. Du kan inte logga och fortsätta.
- 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/Postsedan Ada 2012, D harin/out/invariant, Kotlin harrequire/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 namnetr, så att du kan uttala dig om det. Ochrär bara en identifierare — inget nyckelord.post(result: ...)ellerpost(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 exempelpost(not empty()). - Du kan ha flera av varje.
pre(a) pre(b)är fullt tillåtet och läser ofta bättre änpre(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 annatloän det anroparen skickade in — och då lovar funktionen något helt annat än vad signaturen påstår.constblockerar 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.
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:
- Först
pre(put_idx < capacity() and not full())— bufferten var full, anroparen bröt avtalet. - Sedan
contract_assert(size() <= capacity())— eftersomobservebetyder "logga och kör vidare" fortsatteput()att köra,num_elemsrä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 buffradstdout. Underenforceellerquick_enforcekan 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 avmain():std::setvbuf(stdout, nullptr, _IOLBF, 0);
_IOLBFinnebä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 till202502ä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 denmode(). Och även omdetection_mode::evaluation_exceptionfinns i headern, så rapporterar GCC 16.1predicate_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 motterminate()ändå. - Anropa inte tillbaka in i koden som bröt kontraktet. Vägen till obegränsad rekursion är kort.
- Håll den snabb. Under
observekan 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
observesom 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::expectedeller 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 MVP — minimum 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örstd::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.
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.
Länkar
- Källkod till artikeln – klona och kör alla exempel
- P2900 – Contracts for C++ (själva förslaget)
- P2899 – Contracts for C++: Rationale (varför det ser ut som det gör)
- P1823R0 – Remove Contracts from C++20
- P3097 – Contracts for C++: Virtual functions
- cppreference – Contracts
- cppreference – Function contract specifiers
- cppreference – Header <contracts>
- C.A.R. Hoare – An Axiomatic Basis for Computer Programming, CACM 12(10), 1969
- Eiffel Software – Design by Contract
- Fabian Koehler – Test-driving C++26 contracts with GCC 16.1
Senaste Artiklarna
-
C++26 Contracts
11 september 2026 -
Cargo – konvention framför konfiguration
4 september 2026 -
Referenser & pekare i Rust jämfört med C++
28 augusti 2026 -
När Rust får panik gör C++ ett undantag
21 augusti 2026 -
Rust syntax jämfört med C++
14 augusti 2026 -
Fem skäl att välja Rust över C++
10 augusti 2026 -
Apollo 11 och kvinnan bakom koden
12 april 2026 -
Hur gör man en Ralph Loop?
6 april 2026