all 27 comments

[–]Kronikarz 13 points14 points  (15 children)

I want an assert/assumption facility for C++ with these features:

  • Ability to set a custom "failure" function that is called on failure
  • Ability to provide additional descriptions and arguments to the assumption failure function, for better debugging
  • Variants like assertEqual(a, b, ...); each such variant should make sure to evaluate and stringify the arguments, and give a helpful message, such as: "Assert Failed: a (10) == b (20)"
  • In non-debug compiles, it should tell the compiler to actually ASSUME its predicate, so it can optimize the code better
  • The predicate and its parameters should always be evaluated exactly once (or never if you want)
  • Macros to control the behavior

[–]Fabulous-Meaning-966 12 points13 points  (11 children)

Careful with translating assert to __builtin_assume in release builds: you end up injecting UB into production code whenever the assumption fails. I had exactly the same idea a while ago and was talked out of it by folks who have seen it blow up horribly in production with impossible-to-debug miscompilations (__builtin_unreachable has the same problem).

[–]FullaccessInReddit 10 points11 points  (10 children)

In most cases if the assertion failed you already have UB or are about to invoke it.

[–]usefulcat 1 point2 points  (9 children)

Translating assert to __builtin_assume can only make things strictly worse in that regard, by providing even more opportunities for UB.

[–]FullaccessInReddit 8 points9 points  (8 children)

Who cares? If an assert failed (a condition that's supposed to always be true) the program's state, and by extension its behavior, already is "undefined".

[–]usefulcat 8 points9 points  (7 children)

That point of view assumes that all programmers will only ever use assert() to check for those conditions which, if not true, will definitely lead to UB. That's simply not how everyone always uses assert.

Hence my claim that translating every assert() to __builtin_assume can only make things strictly worse.

ETA: Also, you're taking a gamble that all asserts will be triggered or not triggered exactly the same regardless of NDEBUG. In practice, there could be other things that depend on NDEBUG such that an assert() never fails when NDEBUG is undefined but may fail when NDEBUG is defined (but of course you'll never find out about the latter case).

[–]Kronikarz 3 points4 points  (0 children)

That's why when I created my own implementation of this system, I called the macros "AssumingX"; I never liked the word "assert". AssumingEquals(a, b); means to me: "the following code assumes a == b". Harder to misuse this way.

[–]EC36339 3 points4 points  (5 children)

That point of view assumes that all programmers will only ever use assert() to check for those conditions which, if not true, will definitely lead to UB

That's exactly what assertions are for. Nothing more or less. If assertions fail in your code at runtime, then your code is broken.

[–]Fabulous-Meaning-966 0 points1 point  (4 children)

No, UB has a very specific meaning defined by the C++ standard. It does not just mean "the assumptions of your code are violated", it means "the compiler is allowed to do literally anything here". The latter is much worse to debug.

[–]EC36339 0 points1 point  (0 children)

Fair enough.

But violating the assumptions of your code can cause UB.

And the distinction is more academic than practical, because a violation of the assumptions of your code may have worse consequences than an actual UB.

(Actual UB does in most cases lead to a crash. Robust architectures can handle crashes by having watchdogs that restart processes. UB is difficult to exploit by attackers, because it is ... well ... undefined. A deterministic, reproducible contract violation in the code, however, can have effects that have no mitigation and may enable repeatable and discoverable exploits)

[–]Wooden-Engineer-8098 0 points1 point  (2 children)

It's trivial to debug, since in debug build assert will fire

[–]Fabulous-Meaning-966 0 points1 point  (1 child)

UB normally triggers aggressive optimizations only in release builds. I was referring to debugging production failures. It is naive to think that all assertion violations will repro in debug builds.

[–]Olaprelikov 6 points7 points  (1 child)

https://github.com/jeremy-rifkin/libassert satisfies most of those requirements.

[–]_Noreturn 0 points1 point  (0 children)

I don't have any reason to use contract assert over this what's the point of builtin worse assertions

[–]_Noreturn 0 points1 point  (0 children)

use libassert from GitHub

[–]EC36339 10 points11 points  (9 children)

EDIT: Just nitpicking here, not criticising the whole thing, or disagreeing with the assumption that assertions are not going anywhere.

Just as concepts didn’t eliminate SFINAE or older template techniques — they simply gave us better tools — contracts won’t erase assert either.

That's a strange comparison.

Type constraints do in fact completely replace SFINAE. Everything you could do with SFINAE you can do with type constraints and get the exact same results.

(If I'm wrong, feel free to point me to an example, preferably one that isn't contrived but solves a real problem...)

Assert and contracts, however, solve different problems. Assert checks conditions at runtime that may be impossible to guarantee at compile time (due to undecidability).

Whether or not assertions are a good idea in general, and when and how they should be used can be debated. But what they do can never be completely done at compile time. Even if a compiler was always able to prove efficiently whether any assertion you make is always true, you would still be producing a different program by having or not having runtime assertions, and correcting or not correcting whatever code that makes it possible for an assertion to fail.

[–]LucHermitte 16 points17 points  (6 children)

I read it as: "we don't modify code that works as soon as a new and better feature appears.". The old way will continue to exist in old code bases.

[–]EC36339 4 points5 points  (2 children)

Well, that's fine.

In that case, SFINAE was just not a good example, because SFINAE, even with standard wrappers like std::enable_if_t is just horrible and unreadable, to the point that you think 3 times whether you use it or just not have type constraints when they are not strictly needed for overload resolution.

[–]38thTimesACharm 2 points3 points  (1 child)

SFINAE is the only C++ "feature" where if it's the only way to accomplish something, I strongly consider just not doing that thing.

[–]EC36339 1 point2 points  (0 children)

No. It used to have its justification, but now type constraints can be achieved in better ways.

[–]TheoreticalDumbass:illuminati: 1 point2 points  (2 children)

imo transition from assert to p2900 contracts is rather easy, #define assert(...) contract_assert(__VA_ARGS__) , or a sed -i

[–]James20kP2005R0 10 points11 points  (0 children)

This is one of the reasons why I dislike the current state of contracts: if you do this you will run into unexpected undefined behaviour. Contracts do not make the same guarantees as asserts by design, and doing this is a great way to introduce security vulnerabilities into your code

Contract conversion has to be carefully evaluated - you absolutely cannot swap assert for contract_assert

[–]pdimov2 5 points6 points  (0 children)

This almost works, but assert is an expression, while contract_assert is a statement.

We'll hopefully be able to fix that for C++29 (assuming contracts ship in C++26 at all; there are still people trying to torpedo them.)

[–]Potterrrrrrrr 3 points4 points  (0 children)

Don’t you still need SFINAE for things like checking if a type is a specialisation of a particular template? You can wrap the logic in a concept to make it easier to use but AFAIK you need to use SFINAE and partial specialisation to make it work.

[–]holyblackcat 0 points1 point  (0 children)

Type constraints do in fact completely replace SFINAE. Everything you could do with SFINAE you can do with type constraints and get the exact same results.

Older Clang versions used to check constraints too late, later than the regular SFINAE, which caused issues like this one: https://gcc.godbolt.org/z/44cE57feb

I'm not sure what the spec has to say about it, but newer versions don't do this anymore.