T.concepts.def: Concept definition rules
???
T.20: Avoid "concepts" without meaningful semantics
Reason
Concepts are meant to express semantic notions, such as "a number", "a range" of elements, and "totally ordered."
Simple constraints, such as "has a +
operator" and "has a >
operator" cannot be meaningfully specified in isolation
and should be used only as building blocks for meaningful concepts, rather than in user code.
Example, bad
template<typename T>
concept Addable = has_plus<T>; // bad; insufficient
template<Addable N> auto algo(const N& a, const N& b) // use two numbers
{
// ...
return a + b;
}
int x = 7;
int y = 9;
auto z = plus(x, y); // z = 16
string xx = "7";
string yy = "9";
auto zz = plus(xx, yy); // zz = "79"
Maybe the concatenation was expected. More likely, it was an accident. Defining minus equivalently would give dramatically different sets of accepted types.
This Addable
violates the mathematical rule that addition is supposed to be commutative: a + b == b + a
.
Note
The ability to specify a meaningful semantics is a defining characteristic of a true concept, as opposed to a syntactic constraint.
Example (using TS concepts)
template<typename T>
// The operators +, -, *, and / for a number are assumed to follow the usual mathematical rules
concept Number = has_plus<T>
&& has_minus<T>
&& has_multiply<T>
&& has_divide<T>;
template<Number N> auto algo(const N& a, const N& b) // use two numbers
{
// ...
return a + b;
}
int x = 7;
int y = 9;
auto z = plus(x, y); // z = 18
string xx = "7";
string yy = "9";
auto zz = plus(xx, yy); // error: string is not a Number
Note
Concepts with multiple operations have far lower chance of accidentally matching a type than a single-operation concept.
Enforcement
- Flag single-operation
concepts
when used outside the definition of otherconcepts
. - Flag uses of
enable_if
that appears to simulate single-operationconcepts
.
T.21: Define concepts to define complete sets of operations
Reason
Improves interoperability. Helps implementers and maintainers.
Example, bad
template<typename T> Subtractable = requires(T a, T, b) { a-b; } // correct syntax?
This makes no semantic sense. You need at least +
to make -
meaningful and useful.
Examples of complete sets are
Arithmetic
:+
,-
,*
,/
,+=
,-=
,*=
,/=
Comparable
:<
,>
,<=
,>=
,==
,!=
Enforcement
???
T.22: Specify axioms for concepts
Reason
A meaningful/useful concept has a semantic meaning. Expressing these semantics in an informal, semi-formal, or formal way makes the concept comprehensible to readers and the effort to express it can catch conceptual errors. Specifying semantics is a powerful design tool.
Example
template<typename T>
// The operators +, -, *, and / for a number are assumed to follow the usual mathematical rules
// axiom(T a, T b) { a + b == b + a; a - a == 0; a * (b + c) == a * b + a * c; /*...*/ }
concept Number = requires(T a, T b) {
{a + b} -> T; // the result of a + b is convertible to T
{a - b} -> T;
{a * b} -> T;
{a / b} -> T;
}
Note
This is an axiom in the mathematical sense: something that may be assumed without proof. In general, axioms are not provable, and when they are the proof is often beyond the capability of a compiler. An axiom may not be general, but the template writer may assume that it holds for all inputs actually used (similar to a precondition).
Note
In this context axioms are Boolean expressions.
See the Palo Alto TR for examples.
Currently, C++ does not support axioms (even the ISO Concepts TS), so we have to make do with comments for a longish while.
Once language support is available, the //
in front of the axiom can be removed
Note
The GSL concepts have well defined semantics; see the Palo Alto TR and the Ranges TS.
Exception
Early versions of a new "concept" still under development will often just define simple sets of constraints without a well-specified semantics. Finding good semantics can take effort and time. An incomplete set of constraints can still be very useful:
??? binary tree: rotate(), ...
A "concept" that is incomplete or without a well-specified semantics can still be useful. However, it should not be assumed to be stable. Each new use case may require such an incomplete concepts to be improved.
Enforcement
- Look for the word "axiom" in concept definition comments
T.23: Differentiate a refined concept from its more general case by adding new use patterns.
Reason
Otherwise they cannot be distinguished automatically by the compiler.
Example
template<typename I>
concept bool Input_iter = requires (I iter) { ++iter; };
template<typename I>
concept bool Fwd_iter = Input_iter<I> && requires (I iter) { iter++; }
The compiler can determine refinement based on the sets of required operations. If two concepts have exactly the same requirements, they are logically equivalent (there is no refinement).
This also decreases the burden on implementers of these types since they do not need any special declarations to "hook into the concept".
Enforcement
- Flag a concept that has exactly the same requirements as another already-seen concept (neither is more refined). To disambiguate them, see T.24.
T.24: Use tag classes or traits to differentiate concepts that differ only in semantics.
Reason
Two concepts requiring the same syntax but having different semantics leads to ambiguity unless the programmer differentiates them.
Example
template<typename I> // iterator providing random access
concept bool RA_iter = ...;
template<typename I> // iterator providing random access to contiguous data
concept bool Contiguous_iter =
RA_iter<I> && is_contiguous<I>::value; // ??? why not is_contiguous<I>() or is_contiguous_v<I>?
The programmer (in a library) must define is_contiguous
(a trait) appropriately.
Note
Traits can be trait classes or type traits. These can be user-defined or standard-library ones. Prefer the standard-library ones.
Enforcement
- The compiler flags ambiguous use of identical concepts.
- Flag the definition of identical concepts.
T.25: Avoid negating constraints.
Reason
Clarity. Maintainability. Functions with complementary requirements expressed using negation are brittle.
Example
Initially, people will try to define functions with complementary requirements:
template<typename T>
requires !C<T> // bad
void f();
template<typename T>
requires C<T>
void f();
This is better:
template<typename T> // general template
void f();
template<typename T> // specialization by concept
requires C<T>
void f();
The compiler will choose the unconstrained template only when C<T>
is
unsatisfied. If you do not want to (or cannot) define an unconstrained
version of f()
, then delete it.
template<typename T>
void f() = delete;
The compiler will select the overload and emit an appropriate error.
Enforcement
- Flag pairs of functions with
C<T>
and!C<T>
constraints - Flag all constraint negation
T.26: Prefer to define concepts in terms of use-patterns rather than simple syntax
Reason
The definition is more readable and corresponds directly to what a user has to write. Conversions are taken into account. You don't have to remember the names of all the type traits.
Example
???
Enforcement
???