23. Constrained Add
In a nanosecond-sensitive pricing path, only arithmetic, trivially copyable values should reach math kernels to ensure register-friendly calling and predictable latency. C++20 concepts can enforce these properties at compile time, turning misuses into clean diagnostics. Marking hot functions noexcept and enabling constexpr helps compilers generate leaner, exception-free code.
template<class T>
concept Price = std::is_arithmetic_v<T> && std::is_trivially_copyable_v<T>;
template<Price T>
constexpr T adjust(T px, T tick) noexcept { return px + tick; }
struct Bad { Bad(); int v; };
static_assert(adjust(100, 1) == 101);
Part 1.
Which calls are well-formed: adjust(1.0, 0.5), adjust(std::atomic<int>{}, 1), and adjust(Bad{}, Bad{})? Explain why, and how the constraint plus constexpr/noexcept influences compile-time and code generation.
Part 2.
(1) Why prefer concepts over SFINAE in hot code?
(2) How do constraints affect overload resolution and partial ordering?
(3) Can noexcept here enable better inlining or shrink unwind tables?
(4) Forbid implicit narrowing between float and double using concepts—how?
(5) What ABI or ODR pitfalls arise if Price differs across TUs?
Answer
Answer (Part 1)
adjust(1.0, 0.5) is valid: T deduces to double, which is arithmetic and trivially copyable; it’s constexpr and noexcept. adjust(std::atomic<int>{}, 1) is rejected because std::atomic<int> is neither arithmetic nor trivially copyable; adjust(Bad{}, Bad{}) is rejected because Bad is not arithmetic and not trivially copyable. Constraints are checked during template argument deduction; an unsatisfied constraint removes the candidate from the overload set, yielding clear “constraints not satisfied” diagnostics if nothing matches. noexcept allows the compiler to elide EH paths and possibly shrink unwind metadata; constexpr enables compile-time evaluation when arguments are constant expressions.
Answer (Part 2)
(1) Concepts express intent directly and produce precise diagnostics, avoiding brittle SFINAE patterns. They also prevent accidental instantiations, improving compile-time and API clarity without runtime cost.
(2) Only candidates whose constraints hold participate; more constrained overloads are preferred via subsumption. This reduces ambiguity and guides selection toward the most specific viable template.
(3) Yes. With no cross-boundary exceptions, compilers can remove unwind edges/tables and be more aggressive with inlining and register allocation.
(4) Use a two-parameter template and constrain std::same_as<T,U> for floating types. For example: require (!std::floating_point<T> || !std::floating_point<U> || std::same_as<T,U>).
(5) Divergent Price definitions across translation units can change overload viability, causing ODR violations or link-time mismatches. Centralize the concept in one header and keep it identical across builds/LTO units.