Date: Sat, 18 Feb 2023 14:46:08 +0000
Thoughts on the revival of C++0x Concepts
Introduction
Template is excellent, it has been one of the most successful language features for C++ for its expressiveness and performance. But the template's interfaces have always been lousy, especially when you misuse them. Misusing templates typically causes compilers to generate hundreds of lines of errors. As a solution, C++20 introduced the Concepts, which are some compile-time predicates for constraining templates, making the templates significantly simpler to use, and creating error messages for humans. The C++20 Concepts is definitely one of the most important new features for generic programming, and, only important for generic programming.
Barry Revzin's paper P2279R0 "We need a language mechanism for customization points" pointed out the need for a language-level customization points mechanism by discussing several essential properties and comparing different solutions. We can discover that neither the existing language mechanisms nor library solutions could satisfy all the properties. But we can also find that the C++0x Concepts, which was removed from C++11, could fit most parts of the properties except the "Forwarding Customizations". Although having its shortcomings, the 0x Concepts still tend to be the most suitable solution for customization points, or at least, a good starting point for solving the problem. The 0x Concepts could bring us:
* External polymorphism: A customization point mechanism that is easy to write, simple to use, friendly to newcomers
* Nicer templates: The ability for constraining template arguments
* Definition checking: Compile errors directly from the definition of templates instead of instantiating them
* ...
Although the 0x Concepts are great, we cannot eat the "advantages cake" by simply adopting some of the 0x concepts, a straight adoption will not only brings us the advantages of 0x Concepts, but also the most annoying and divergent parts. So before selecting the 0x Concepts to be the solution of customization points, we need to take a further discussion of certain problems:
* Overlapping with the 20 Concepts we have today
* Do we need definition checking?
* Implicit model or explicit model(concept_maps)?
* "refinement" relations among concepts like object-oriented inheritance?
Given my limited knowledge of C++, I could hardly give any concrete and mature solution to these problems. But at least I could share some of my thoughts on these.
Definition checking
The following code provides a concept to describe equality comparable types:
concept equality_comparable<typename T> {
bool operator==(const T& x, const T& y);
bool operator!=(const T& x, const T& y) { return !(x==y); }
}
When applying the concept EqualityComparable to a template, any use of the operations which haven't been guaranteed by the concepts will cause a compilation error. So the following code will not compile:
template <input_iterator Iter, typename Val>
requires equality_comparable<Iter::value_type, Val>
Iter find(Iter first, Iter last, Val v)
{
while(first < last && !(*first == v)) // error : no < in EqualityComparable
++first;
return first;
}
At first glance, it seems to be a great feature of 0x concepts. But if we go deeper into the 0x concepts, we could find a problem:
* How to interoperate between the constrained templates and unconstrained templates?
To solve the problem, the 0x Concepts introduced the late_check blocks to delay checks to instantiation. But quickly the implementation experience showed that the non-trivial combinations of concept_maps and late_checks could lead to invisible violations of the type system. And another approach to solving the problem, axioms, met difficulties when getting the notion of an axiom accepted.
My suggestion is; Considering the risks and the reward, if we could adopt some of the 0x Concepts into the language, don't include the definition checking, we can just delay checks to instantiation like what 20 Concepts do. The definition checking, if needed, could be added later in some backward-compatible way.
The concept_map
Should the concept_maps be explicit? To answer this problem, we should clarify the role of 0x Concepts in our code. From my point of view, the 0x concepts should be a commonly-used feature for the experts as well as the Joe Coder. We need to make the concept_maps implicit by default. And provide an opt-in way to make them explicit because the experts relatively pay no attention to the complexity of language mechanisms, For example:
concept printable<typename T> {
void print() { cout << "something cool." << endl; }
}
template<printable T>
void constrained(T const& p) {
p.print();
}
struct some_struct {
};
int main() {
some_struct i;
constrained(i); //the concept_map is generated implicitly
}
And if we want to make every concept_map of the printable concept being explicit, we could add a explicit before the concept:
explicit concept printable<typename T> { //forcing concept_maps to be explicit
void print() { cout << "something cool." << endl; }
}
template<printable T>
void constrained(T const& p) {
p.print();
}
struct some_struct {
};
int main() {
some_struct i;
constrained(i); //now this won't compile...
}
Refinement relation
The 0x Concepts use "refinement" relations to describe the hierarchy of concepts, such as:
concept bidirectional_iterator<typename T>
: forward_iterator<T> { //A bidirectional_iterator is a kind of forward_iterator
}
The refinements introduced a serious inflexibility into the 0x Concepts system because the relations of concepts are typically not hierarchies. My suggestion is to use the "composition" like 20 Concepts instead of the "refinements", this could be done with the require clauses:
concept bidirectional_iterator<typename T> {
requires forward_iterator<T>; //A bidirectional_iterator is a kind of forward_iterator
}
Overlaps with the C++20 Concepts
The purpose of 0x Concepts is the same as the 20 Concepts, so it's not surprising to find there is an overlapping between them. Maybe we can solve this problem by reusing 20 Concepts facilities. There is a large unexplored design space. I think that implicitly generating 20 Concepts from 0x Concepts could be such a solution. For example, the following code:
concept printable<typename T> {
void print() { cout << "something cool." << endl; }
}
template<printable T>
void constrained(T const& p) {
p.print();
}
concept_map printable<some_type> {...}
is the same as:
concept printable_0x<typename T> {
void print() { cout << "something cool." << endl; }
}
template <typename T>
concept printable_20 = requires(T t) {
{t.print()} -> std::convertible_to<void>;
};
template<printable_20 T>
void constrained(T const& p) {
p.print();
}
concept_map printable_0x<some_type> {...}
The 20 Concepts might need some tweaks to support the implicit generation.
Introduction
Template is excellent, it has been one of the most successful language features for C++ for its expressiveness and performance. But the template's interfaces have always been lousy, especially when you misuse them. Misusing templates typically causes compilers to generate hundreds of lines of errors. As a solution, C++20 introduced the Concepts, which are some compile-time predicates for constraining templates, making the templates significantly simpler to use, and creating error messages for humans. The C++20 Concepts is definitely one of the most important new features for generic programming, and, only important for generic programming.
Barry Revzin's paper P2279R0 "We need a language mechanism for customization points" pointed out the need for a language-level customization points mechanism by discussing several essential properties and comparing different solutions. We can discover that neither the existing language mechanisms nor library solutions could satisfy all the properties. But we can also find that the C++0x Concepts, which was removed from C++11, could fit most parts of the properties except the "Forwarding Customizations". Although having its shortcomings, the 0x Concepts still tend to be the most suitable solution for customization points, or at least, a good starting point for solving the problem. The 0x Concepts could bring us:
* External polymorphism: A customization point mechanism that is easy to write, simple to use, friendly to newcomers
* Nicer templates: The ability for constraining template arguments
* Definition checking: Compile errors directly from the definition of templates instead of instantiating them
* ...
Although the 0x Concepts are great, we cannot eat the "advantages cake" by simply adopting some of the 0x concepts, a straight adoption will not only brings us the advantages of 0x Concepts, but also the most annoying and divergent parts. So before selecting the 0x Concepts to be the solution of customization points, we need to take a further discussion of certain problems:
* Overlapping with the 20 Concepts we have today
* Do we need definition checking?
* Implicit model or explicit model(concept_maps)?
* "refinement" relations among concepts like object-oriented inheritance?
Given my limited knowledge of C++, I could hardly give any concrete and mature solution to these problems. But at least I could share some of my thoughts on these.
Definition checking
The following code provides a concept to describe equality comparable types:
concept equality_comparable<typename T> {
bool operator==(const T& x, const T& y);
bool operator!=(const T& x, const T& y) { return !(x==y); }
}
When applying the concept EqualityComparable to a template, any use of the operations which haven't been guaranteed by the concepts will cause a compilation error. So the following code will not compile:
template <input_iterator Iter, typename Val>
requires equality_comparable<Iter::value_type, Val>
Iter find(Iter first, Iter last, Val v)
{
while(first < last && !(*first == v)) // error : no < in EqualityComparable
++first;
return first;
}
At first glance, it seems to be a great feature of 0x concepts. But if we go deeper into the 0x concepts, we could find a problem:
* How to interoperate between the constrained templates and unconstrained templates?
To solve the problem, the 0x Concepts introduced the late_check blocks to delay checks to instantiation. But quickly the implementation experience showed that the non-trivial combinations of concept_maps and late_checks could lead to invisible violations of the type system. And another approach to solving the problem, axioms, met difficulties when getting the notion of an axiom accepted.
My suggestion is; Considering the risks and the reward, if we could adopt some of the 0x Concepts into the language, don't include the definition checking, we can just delay checks to instantiation like what 20 Concepts do. The definition checking, if needed, could be added later in some backward-compatible way.
The concept_map
Should the concept_maps be explicit? To answer this problem, we should clarify the role of 0x Concepts in our code. From my point of view, the 0x concepts should be a commonly-used feature for the experts as well as the Joe Coder. We need to make the concept_maps implicit by default. And provide an opt-in way to make them explicit because the experts relatively pay no attention to the complexity of language mechanisms, For example:
concept printable<typename T> {
void print() { cout << "something cool." << endl; }
}
template<printable T>
void constrained(T const& p) {
p.print();
}
struct some_struct {
};
int main() {
some_struct i;
constrained(i); //the concept_map is generated implicitly
}
And if we want to make every concept_map of the printable concept being explicit, we could add a explicit before the concept:
explicit concept printable<typename T> { //forcing concept_maps to be explicit
void print() { cout << "something cool." << endl; }
}
template<printable T>
void constrained(T const& p) {
p.print();
}
struct some_struct {
};
int main() {
some_struct i;
constrained(i); //now this won't compile...
}
Refinement relation
The 0x Concepts use "refinement" relations to describe the hierarchy of concepts, such as:
concept bidirectional_iterator<typename T>
: forward_iterator<T> { //A bidirectional_iterator is a kind of forward_iterator
}
The refinements introduced a serious inflexibility into the 0x Concepts system because the relations of concepts are typically not hierarchies. My suggestion is to use the "composition" like 20 Concepts instead of the "refinements", this could be done with the require clauses:
concept bidirectional_iterator<typename T> {
requires forward_iterator<T>; //A bidirectional_iterator is a kind of forward_iterator
}
Overlaps with the C++20 Concepts
The purpose of 0x Concepts is the same as the 20 Concepts, so it's not surprising to find there is an overlapping between them. Maybe we can solve this problem by reusing 20 Concepts facilities. There is a large unexplored design space. I think that implicitly generating 20 Concepts from 0x Concepts could be such a solution. For example, the following code:
concept printable<typename T> {
void print() { cout << "something cool." << endl; }
}
template<printable T>
void constrained(T const& p) {
p.print();
}
concept_map printable<some_type> {...}
is the same as:
concept printable_0x<typename T> {
void print() { cout << "something cool." << endl; }
}
template <typename T>
concept printable_20 = requires(T t) {
{t.print()} -> std::convertible_to<void>;
};
template<printable_20 T>
void constrained(T const& p) {
p.print();
}
concept_map printable_0x<some_type> {...}
The 20 Concepts might need some tweaks to support the implicit generation.
Received on 2023-02-18 14:46:13