项目作者: cjdb

项目描述 :
Pre-conditions, post-conditions, and assertions, all available at compile-time
高级语言: C++
项目地址: git://github.com/cjdb/constexpr-contracts.git
创建时间: 2019-08-28T21:38:12Z
项目社区:https://github.com/cjdb/constexpr-contracts

开源协议:Apache License 2.0

下载


constexpr-contracts

C++20 was looking good to have contracts until the C++ Standards Committee meeting in Cologne,
Germany of 2019. There, some strong turbulence happened, and the committee ultimately felt that
significantly more work was necessary before contracts are ready for Standard C++.

As one of the authors requesting contracts be removed from the C++20 Working Paper, I’d like to
offer a consolation library, which emulates how I’d like contracts to behave.

Prerequisites

  1. CMake 3.14 or later
  2. Conan 1.18 or later (if using Conan)
  3. A standard library that supports std::is_constant_evaluated (e.g. libstdc++11-9 or libc++-9)

Installation

Without Conan

You can clone this repository and place the directory cjdb in your own include directory.

With Conan

  1. Add https://api.bintray.com/conan/cjdb/cjdb to your list of remotes.
  2. Add constexpr-contracts to your list of required packages.

Linking in CMake

To link in CMake, you’ll need to use find_package(constexpr-contracts REQUIRED) to import the library.
To link against the library, use target_link_libraries(target PRIVATE cjdb::constexpr-contracts).

Usage

This library provides three macros:




















Contract type Macro
Pre-condition CJDB_EXPECTS
Assertion CJDB_ASSERT
Post-condition CJDB_ENSURES

As the name of this library implies, you can use all of these macros in constant expressions. Your
contracts are always checked at compile-time. If the contract’s predicate isn’t satisfied, then the
program won’t compile. This is a huge advantage over using <cassert> or the GSL’s Expects and
Ensures macros.

When optimisations are diabled and NDEBUG is not defined as a macro, the contract will check your
predicate at run-time. If the predicate fails, then a diagnostic will be emit, and the program will
crash.

When optimisations are enabled, and NDEBUG remains undefined, the program will emit a diagnostic
for failed predicates, but the program continues to run, under the assumption that the
predicate was actually true
. Your program will be slightly larger than if you hadn’t used the
contract, but this might be useful for identifiying cases where users report bugs (in a release
build) that aren’t being caught by your assertions.

When optimisations are disabled, but NDEBUG is defined as a macro, the program will crash when
the predicate fails, but no diagnostic is emitted. Code generation also appears to be significantly
worse.

When both optimisations and NDEBUG are enabled, the optimiser is allowed to make assumptions that
could improve code generation and no diagnostics are emitted. Toy examples demonstrate better code
generation than when the contract isn’t used. See for yourself.












































































Optimisations enabled DNDEBUG enabled Predicate evaluation Notes
Constant expression
N/A N/A false Program does not compile
N/A N/A true Program compiles
Run-time expression
No No false
A diagnostic is written to stderr at run-time and then the program crashes.
No Yes false
The generated code will probably be slightly different than if NDEBUG
hadn’t been defined.



A diagnostic is written to stderr at run-time and then the program crashes.
Yes No false
Optimiser considers predicate as absolute truth, which leads to optimisations that won’t
make sense with the given incorrect input.



A diagnostic is written to stderr at run-time. The program won’t crash, but
it’ll probably give incorrect output.
Yes Yes false
Optimiser considers predicate as absolute truth, which leads to optimisations that may
result in a faster run-time or a smaller binary.



The program won’t crash, but it’ll probably give incorrect output.
No No true
The program will take a performance hit (subject to the evaluation of the predicate plus
a check to see that the predicate is true), but will otherwise behave as if
the predicate didn’t exist.
No Yes true
The program will take a performance hit (subject to the evaluation of the predicate plus
a check to see that the predicate is true), but will otherwise behave as if
the predicate didn’t exist.
Yes No true
The optimiser considers the predicate to mean the absolute truth, and may make
assumptions about code following the contract. This might lead to optimisations
resulting in a faster run-time or smaller binary.



Correct program output is expected, and code generation is expected to be
better than without optimisations, but the program will not be as small as when built
using -DNDEBUG.
Yes Yes true
The optimiser considers the predicate to mean the absolute truth, and might make
aggressive assumptions about code following the contract. This may lead to
optimisations resulting in a faster run-time, a smaller binary, or both.



Correct program output is expected. Performance is subject to the level of optimisation,
but this configuration is expected to result in the best code generation.

*Rudimentary testing has identified that neither GCC nor Clang perform optimisations before
the contract.

Assertions

You should use assertions to indicate when a programming error has occurred in the middle of your
code. For example, the following program performs division operation, and checks that f never
returns 0. If f returns zero, then the programmer has communicated that there’s a logic error
somewhere.

  1. // file: example1.cpp
  2. #include <cjdb/contracts.hpp>
  3. #include <concepts>
  4. int main()
  5. {
  6. std::integral auto x = f();
  7. CJDB_ASSERT(x != 0);
  8. std::cout << (5 / x) << '\n';
  9. }

The above program will behave as if the assertion is not present in the event x != 0. If, however,
x == 0, the program will print the following message in debug-mode and then crash.

  1. ./example.cpp:8: assertion `x != 0` failed in `int main()`

In this case, nothing is different with -O3 -DNDEBUG, but take a look at what
happens when the compiler can see what we’re expecting.

Pre-conditions

You should use a pre-condition to indicate that the expected input for a function is not met.
Although it can’t be compiler-enforced, you should only use a pre-condition to check that a
parameter doesn’t meet its expected input values; everything else should be checked using an
assertion. This will help your users determine when they have provided bad parameters, and when
their state is the cause of a logic error.

  1. // example2.cpp
  2. #include <cjdb/contracts.hpp>
  3. #include <string_view>
  4. class person {
  5. public:
  6. constexpr explicit person(std::string_view first_name, std::string_view surname, int age)
  7. : first_name_{first_name}
  8. , surname_{surname}
  9. , age_{(CJDB_EXPECTS(age >= 0), age)} // parens are necessary!
  10. {}
  11. private:
  12. std::string first_name_;
  13. std::string surname_;
  14. int age_;
  15. };
  16. auto const ada_lovelace = person{
  17. "Ada",
  18. "Lovelace",
  19. -1
  20. };

In our second example, we’re establishing that a person’s age must be 0 or greater. We do this
before we initialise age_, so that we know that the expectation is met before we ever get a chance
to use it. The run-time diagnostic we get is:

  1. ./example2.cpp:10: pre-condition `age >= 0` failed in `person::person(std::string_view, std::string_view, int)`

Post-conditions

Finally, post-conditions allow us to provide a guarantee that our invariants hold upon exiting a
function. These are a bit different in usage: instead of providing just an expression, the intended
usage is to place your post-condition in a return-statement. As such, there should only be one
post-condition per return-statement in a function.

  1. #include <cjdb/contracts.hpp>
  2. int f()
  3. {
  4. auto result = g();
  5. return CJDB_ENSURES(result >= 0), result;
  6. }

This usage lets us chain post-conditions before our result is returned. As with pre-conditions and
assertions, the diagnostic indicates what kind of contract was violated.

constexpr-contracts in constant expressions

Notice that person::person is constexpr. constexpr-contracts has a huge advantage over both
assert and static_assert, because it can be evaluated at compile-time or at run-time,
depending on the context. If we wanted to constant-initialise ada_lovelace, then we’d get a
compile-time diagnostic instead. They’re fairly difficult to read, so I’ve put one below to guide
you.

Clang

  1. constexpr auto ada_lovelace = person{
  2. ^ ~~~~~~~
  3. ../include/cjdb/contracts.hpp:57:4: note: subexpression not valid in a constant expression
  4. __builtin_unreachable();
  5. ^
  6. ../test/person.cpp:8:10: note: in call to 'contract_impl(false, {&"../test/person.cpp:8: pre-condition `age >= 0` failed in `%s`\n"[0], 62}, {&__PRETTY_FUNCTION__[0], 73})'
  7. , age_{(CJDB_EXPECTS(age >= 0), age)}
  8. ^
  9. ../include/cjdb/contracts.hpp:29:27: note: expanded from macro 'CJDB_EXPECTS'
  10. #define CJDB_EXPECTS(...) CJDB_CONTRACT_IMPL("pre-condition", __VA_ARGS__)
  11. ^
  12. ../include/cjdb/contracts.hpp:66:4: note: expanded from macro 'CJDB_CONTRACT_IMPL'
  13. ::cjdb::contracts_detail::contract_impl(static_cast<bool>(__VA_ARGS__), \
  14. ^
  15. ../test/person.cpp:16:31: note: in call to 'person({&"Ada"[0], 3}, {&"Lovelace"[0], 8}, -1)'
  16. constexpr auto ada_lovelace = person{
  17. ^

That’s a lot of error to say that age < 0! We can fortunately ignore 90% of the above. The only
useful information in the entire diagnostic for our purposes are the first eight lines.

  1. constexpr auto ada_lovelace = person{
  2. ^ ~~~~~~~
  3. ../include/cjdb/contracts.hpp:57:4: note: subexpression not valid in a constant expression
  4. __builtin_unreachable();
  5. ^
  6. ../test/person.cpp:8:10: note: in call to 'contract_impl(false, {&"../test/person.cpp:8: pre-condition `age >= 0` failed in `%s`\n"[0], 62}, {&__PRETTY_FUNCTION__[0], 73})'
  7. , age_{(CJDB_EXPECTS(age >= 0), age)}
  8. ^

Here, we see that constexpr auto ada_lovelace = person{ is the offending call-site, and that
CJDB_EXPECTS(age >= 0) is the contract that we broke. Literally everything else is helper
information that the compiler needs to give us, but we don’t need.

GCC

  1. In file included from ../test/person.cpp:1:
  2. ../test/person.cpp:20:1: in constexpr expansion of person(std::basic_string_view<char>(((const char*)"Ada")), std::basic_string_view<char>(((const char*)"Lovelace")), -1)’
  3. ../test/person.cpp:8:10: in constexpr expansion of cjdb::contracts_detail::contract_impl(((int)(((int)age) >= 0)), std::basic_string_view<char>(((const char*)"../test/person.cpp:8: pre-condition `age >= 0` failed in `%s`\012")), std::basic_string_view<char>(((const char*)(& __PRETTY_FUNCTION__))))’
  4. ../include/cjdb/contracts.hpp:57:25: error: __builtin_unreachable()’ is not a constant expression
  5. 57 | __builtin_unreachable();
  6. | ~~~~~~~~~~~~~~~~~~~~~^~

Sadly, GCC’s diagnostic is much more technical, and harder to read. You’ll need to look for
cjdb::contracts_detail::contract_impl(((int)(((int)age) >= 0)).
cjdb::contracts_detail::contract_impl is an implementation detail, so only look for it in
diagnostics, as it is subject to change. As such, I’m working on a way to provide a much better
diagnostic.

Pitfalls

Because we’re peering into the definition here, the code is somewhat brittle: any user-facing
declaration needs to match the definition right down to parameter names; otherwise the contract won’t
make much sense to the user.