9 ms·
Adding design-by-contract conditions to C++ via a GCC plugin
- MuffinFlavored 4y agoThe B-Tree invariant example in the unit test seems like a bunch of assertions. Feels very similar to unit tests? Am I missing something? Assert at runtime or assert at unit test time, it's assertions all the way down? Not sure the benefit of some weird @decortator-like syntax to achieve 5-10 lines of assertions?
- vlovich123 4y agoVery similar but I suspect most people don’t use asserts properly so this is more of an opinionated approach that makes it more difficult to misuse.
- renox 4y agoIf I remember well(), the idea between contracts is that they can be enabled at runtime, so very different from UT. Also different from assertions in that 'class' invariants applies to every public method of the class. : All I know about 'Design by Contract' I learned it in the (very good IMHO) book Object Oriented Software Construction by Bertrand Meyer (he made the Eiffel language).
- unsafecast 4y agoYou missed a backslash before your asterisks :)
- gavinray 4y agoContracts and Invariants don't replace unit tests, they're complementary This is the big mix up with DbC in general I think. Think of it more like sanity-tests and assertions that help ensure your program isn't in some haywire state. A good example with the BTree is that the invariants ensure the properties are enforced at runtime, but you still need test to invoke those properties. These sorts of invariants mix really well with property-style tests: You write some declarative specification for a set of boundaries on inputs/outputs, and then subject your classes to the generated inputs. If your tests pass, then all your invariants have held and you know at the bare minimum, the data structures are doing what you intend them to do. Above that you can do load-testing and subject the invariants to scale-factors.
- MuffinFlavored 4y agoDbC?
- andreareina 4y agoDesign by Contract
- kgeist 4y agoWhat's the advantage of invariants over unit testing? Seems like there must be lot of overhead at runtime. >I was ending up with garbage, and not realizing it until I had visualized it with Graphviz! >Imagine if we had invariants that we could assert after every property change to the tree. Have you tried writing unit tests? What didn't work about them that you decided to try invariants?
- nimish 4y agoThey are declarative vs imperative and sufficiently smart tooling exists such that they can be checked and enforced statically, see liquid haskell and "refinement" typing for an example. Formal verification starts to become a possibility -- enforcing both conformance and documentation. Tests are fine too but more tedious work.
- charcircuit 4y agoThis isn't a replacement for unit tests. This is an alternate syntax to using asserts. You would still want unit tests to see if those asserts are being violated.
- Someone 4y agoNot the writer, but it seems obvious to me. It’s a luxury version of assert. You leave them enabled in debug builds to get better bug reports from testers. Those testers may hit cases you forgot to write unit tests for. Of course, you can also forget to write invariants, or write invariants that are less tight than they should be, but I think it often is easier to write invariants than to write exhaustive unit tests. Firstly, writing “p should always be a prime” is clearer than writing “if p is a prime, and you call f, p should be a prime afterwards”, and secondly, invariants can apply to multiple methods that you, otherwise, would have to write separate tests for (“foo keeps p a prime”, “bar keeps p a prime”, “bar keeps p a prime when called with a null argument”, “baz keeps p a prime if it throws an exception”, etc) Also, invariants, IMO, can be way better documentation than unit tests. Finally, invariants leave open the possibility of using a theorem prover to (dis)prove that they hold.
- pstrateman 4y agoIt's more likely that they'll get updated correctly when the surrounding code is changed. Unit testing requires someone to enforce they get updated.
- josephg 4y agoUsing invariants like this is now my preferred programming style. Almost all of my complex container classes have a dbg_check() method which checks all the internal invariants hold, and panics if not. When testing and debugging, I’ll add calls to dbg_check() after each mutation of my data structure. And then I write a fuzzer which exercises my API in a loop. Each iteration, I check that my invariants still hold. The nice thing about this sort of invariant checking is that it makes it very fast and easy to narrow in on bugs. The crash happens right after a change which made the data invalid, not later when the invalid data is accessed. And a few dozen lines of fuzz testing can find an extraordinary quantity of subtle bugs. It’s remarkable. Devastating for the ego, but remarkable. Edit: Simple dbg_check implementation example: https://github.com/josephg/diamond-types/blob/3eb48478fd879e9f2bb9bcc6062762fbf2f8db21/src/causalgraph/check.rs https://github.com/josephg/diamond-types/blob/3eb48478fd879e... And here's a simple fuzz tester: https://github.com/josephg/jumprope-rs/blob/318e87d3aae1b2a0333042c7e75d30179c162f21/tests/test.rs#L202-L261 https://github.com/josephg/jumprope-rs/blob/318e87d3aae1b2a0...
- SoylentYellow 4y agoDo you have an example of this you can share?
- josephg 4y agoUpdated the comment.
- jandrewrogers 4y agoI do something very similar, and as you say it is highly effective. I don't recall ever finding a bug later in code that was properly tested this way. It is not an inexpensive type of testing when done well but I think it is a pragmatic way of producing code that can approach the robustness of formal verification in practice with a lot less work, so the ROI is high.
- josephg 4y ago> It is not an inexpensive type of testing when done well I find the ROI is much higher than most forms of unit testing. Fewer lines of testing code finds more bugs. And with invariant checking and a seeded random number generator, you can reproduce any failures and (usually) find the bug pretty quickly. They do take more brain cells per line of testing code though, especially at first. This sort of testing also addresses my biggest frustration with unit testing. Unit tests have diminishing returns as you add more tests. Normally by the time I have "enough" unit tests, it becomes exhausting to refactor my code because of the giant pile of tests I need to rewrite. Fuzz tests are much easier to update. (That said, fuzz testing can't replace unit tests entirely. Especially when you have a lot of methods in your API).
- ghotli 4y agoInvariant vs unit tests aside. Doesn't this being a gcc plugin make it inherently unattractive? Wouldn't want to have to rip that code out if I wanted the code to behave on a non-gcc toolchain. Otherwise, seems cool if that doesn't matter / will never matter in the lifetime of the codebase
- yjftsjthsd-h 4y ago> Wouldn't want to have to rip that code out if I wanted the code to behave on a non-gcc toolchain. I wonder if it can be wrapped in #ifdef ?
- loeg 4y agoBetter to define a macro that evaluates to the contract/invariants on GCC and empty on other compilers, rather than copying the ifdef/else/endif logic around to every usage.
- gavinray 4y agoYeah sure, there's nothing preventing you from using "constexpr" so it's compile-time evaluated and inlined: (Please no more macros, it's 202- I mean, 2023!) static constexpr bool DEBUG = true; [[demo::invariant]] [[gnu::used]] void check_invariants() { if constexpr (!DEBUG) return; assert(top >= 0 && top <= MAX_SIZE); }
- celrod 4y agoNote that `assert`s are disabled if you define the macro `NDEBUG`, e.g. https://godbolt.org/z/hMWo8KM7q https://godbolt.org/z/hMWo8KM7q CMake defines the macro in release builds: https://github.com/Kitware/CMake/blob/e1eacbe2c522a8bf9a82afb483fe395140240614/Modules/Compiler/GNU.cmake#L58-L60 https://github.com/Kitware/CMake/blob/e1eacbe2c522a8bf9a82af... Would be nice to have a non-macro solution for controlling behavior at configure time, but the `NDEBUG` macro is basically already your `DEBUG` constexpr.
- msla 4y ago> Wouldn't want to have to rip that code out if I wanted the code to behave on a non-gcc toolchain. I can only imagine some companies would literally never have the problem of switching to a new compiler toolchain.
- kokonoko 4y agoCreating a separate gcc plugin for this seems unnecessary if I am not misunderstanding something. Just ensure the invariant function is called at every method, in debug builds maybe. You could of course change the object properties without using a method but then you have bigger problems.
- Volt 4y ago> Just ensure the invariant function is called at every method You make this sound way easier than I would expect it to be in the general case.
- loeg 4y agoYou add something like: DCHECK(myInvariants()); to your method bodies. Sure, it makes it possible to forget, but it's not that hard to use.
- int_19h 4y agoA big part of DbC is that contracts are part of the public interface, not the implementation. This becomes especially important once you mesh them with Simula-style OOP, as derived classes need a way for overridden methods to widen or narrow the inherited contract.
- gavinray 4y agoThey're suggesting you write the output of the codegen shown in the image here: https://gavinray97.github.io/static/images/ghidra-check-invariants.png https://gavinray97.github.io/static/images/ghidra-check-inva... Yeah you totally could, and before writing this plugin it's what I was doing, haha. As there is a human involved, it is very error prone though, and boy is it tedious!
- Animats 4y agoI thought "contracts" were dropped out of C++20.[1] Entry and exit assertions, and invariants, are powerful, especially when coupled with a proof of correctness system. But they're a tough retrofit. Invariants have some issues. The general idea is that the invariant has to be true when control is outside the object. This was once the core idea of the object concept, although it's been somewhat forgotten. A big issue is, when does control enter and exit the object? What if you call a public member function from inside the object? Did you re-enter? What if you call out of an object to something that calls back in? What about recursion? What if another thread enters? Objects need clarity on the inside/outside issue for invariants to work. This is quite possible but a tough retrofit. Invariants need syntax for talking about arrays and parts therof. You need quantifiers, or something like them. Lambdas? From back when I did this sort of thing, decades ago, a simple SAT solver can eliminate the need to check over 90% of assertions at run time. So you want something like that, rather than trying to check everything at run time. Otherwise, nobody will keep the checks turned on. All this is quite do-able. Most attempts to do it have suffered from academic overreach - the technology is pushed by people in love with formal methods, and the result is too complicated for routine use. [1] https://www.reddit.com/r/cpp/comments/cmk7ek/what_happened_to_c20_contracts/ https://www.reddit.com/r/cpp/comments/cmk7ek/what_happened_t...
- gavinray 4y ago> I thought "contracts" were dropped out of C++20.[1] You're right, but (I'm not an authority, someone from /r/cpp would be better to speak here probably) I think after the Kona meeting recently they were back on the charter for next iteration Specifically, GCC 13 has had support for Contracts in main since November: https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=ea63396f6b08f88f1cde827e6cab94cd488f7fa7 https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=ea63396f6b08f8... > A big issue is, when does control enter and exit the object? What if you call a public member function from inside the object? Did you re-enter? If you check the generated code from the end of the blogpost (see the bit that shows the Ghidra dissasembly) or the notes section, the pattern used in the plugin is: T function() { check_invariants(); // unless we're in constructor result = function_body(); check_invariants(); // unless we're in a destructor return result; } This does mean that a single member call can cause multiple cascading calls to check_invariants() If "this.foo()" calls "this.bar()" which calls "this.baz()", etc. But it's not avoidable, because in this open-world assumption we assume any member call can potentially mutate the state of the member.
- apankrat 4y agoTangentially related - Few years ago started using asserts as an ad-hoc documentation mechanism for invariants and then also started shipping them in production builds. When triggered, asserts grab the stack trace, write it into the application log and give an option of sending a bug report. And then they shut down the program. Was scary at first, but the initial pain is absolutely worth it. This flushed hundreds of absolutely crazy edge cases, improved code quality and stability tremendously. It also forced writing cleaner code to begin with and sped up debugging while in development. Now have about 3K asserts in 250 KLoC code base. Can't recommend this practice strong enough.
- civopsec 4y agoI looked at this thread in the morning. Then I waited the whole workday to get home and read anecdotes like yours. This is seriously cool. I never understood why people don’t talk more about this style of programming.
- kaashif 4y agoI think this is very interesting: > The working code for insertion of the check_invariants statements was a stroke of dumb luck after 15 hours, from one of the suggestions given by Github Copilot. GitHub Copilot coming in handy when documentation is lacking? Will widespread use of tools like this reduce the incentive, over time, to write good documentation? Something to think about.
- gavinray 4y agoI prototyped much of this code using ChatGPT and Copilot. I won't go so far as to say, "I couldn't have done it without you, ma!", but it certainly would have taken me much longer. In terms of learning the actual API, they were worth their weight in gold. One trick to use is that Copilot can read up to (apparently) 20 other tabs that you have open. So, I will open the source code containing the API methods I need in my other tabs and close everything else. > The working code for insertion of the check_invariants statements was a stroke of dumb luck after 15 hours, from one of the suggestions given by Github Copilot. What is funny here is that it suggested something radically different. I was going down the rabbit hole I chronicled here: https://stackoverflow.com/questions/74964153/gcc-gimple-c-api-how-to-insert-a-call-to-a-member-function-from-another-memb https://stackoverflow.com/questions/74964153/gcc-gimple-c-ap... One strategy I use in unfamiliar territory is to repeatedly generate the set of 10 Copilot suggestions and see if anything sticks out. I had gone through maybe 3-4 rounds when it spits out one that looks nothing like what I currently have. I try that suggestion, and it works. I shake my head. What the fuck. Alright, I'll take it. It wanted to do a regular C-style call that had no member namespace, which by sheer luck happens to work due to how C++ lookup works.
- sgerenser 4y agoOne trick to use is that Copilot can read up to (apparently) 20 other tabs that you have open. So, I will open the source code containing the API methods I need in my other tabs and close everything else. :shocked pikachu: This can’t possibly be true? How would it even do that, and unless it was off by default, that seems like a huge privacy problem. Edit: never mind, I was reading tabs as “browser tabs” not “editor tabs.”
- loeg 4y agoThe syntax is cool, but you can always check and assert invariants and contract semantics without the special syntax.
- WalterBright 4y agoI added dbc to C++ back in the early 90s. https://www.digitalmars.com/ctg/contract.html https://www.digitalmars.com/ctg/contract.html
- gavinray 4y agoThe inspiration was taken directly from D, I credit as much in the README =) https://github.com/GavinRay97/gcc-invariant-plugin#gcc-design-by-contract-invariant-plugin https://github.com/GavinRay97/gcc-invariant-plugin#gcc-desig... > Inspired wholly by the D Programming Language's invariant feature. How I wish D had caught on as the popular language rather than C++.
- badtuple 4y agoDoes anyone have an example of a codebase that uses Design By Contract to an extreme extent? You really only get a sense of the power of patterns like this when you see that power abused. I've greatly enjoyed writing 2 very small codebases in that style...but that doesn't mean I did it well or that it'd hold up after years of maintenance from a revolving door of developers.
- TazeTSchnitzel 4y agoI believe Rust had some form of contract enforcement in its early days, but they eventually removed it because it had too many problems. I wish I could find the post about it, but Google is useless these days.
- mihaigalos 4y agoThe contracts crate [1] supports this via macros. I actually prefer the invariant section above the definition, but do admit the pre and post in C++ look better. [1] https://docs.rs/contracts/latest/contracts/ https://docs.rs/contracts/latest/contracts/
- skocznymroczny 4y agoD has had support for pre/post contracts and invariants for a long time, but I haven't seen them used in many projects. I think it's one of those niche uses, or something that sounds like it'd be fun to have but in the end ends up being annoying to maintain.
- _a_a_a_ 4y agoIrritating they are, but utterly, utterly worth it if you care about quality.
- zelphirkalt 4y agoIn other languages adding something like this is possible from within the language in basically any syntax you want. No need for some plugin on another layer like GCC.
- rurban 4y agoSuch assertions are nice, but easier done with normal assert(). No need for a plugin. Much better is formal verification though, which I do on all my containers. cbmc is your friend.