What are the use cases of C++20 Concepts? - c++

I found about Concepts while reviewing C++20 features. I found that they add validation to templates arguments but apart from that I don't understand what are the real world use cases of C++20 concepts.
C++ already has things like std::is_integral and they can perform validation very well.
I'm sure I am missing something about C++20 concepts and what it enables.

SFINAE (see here & here) was an accidentally Turing complete sublanguage that executes at overload resolution and template specialization selection time.
Turns out it is used a lot in template code.
Concepts and requires clauses are an attempt to take that accidentally useful language feature and make it suck less.
The origin of concepts was going to have 3 pieces; (a) describing what is required for a given bit of template code in a clean way, (b) also provide a way to map other types to satisfy those requirements non-intrusively, and (c) check template code so that any type which satisfies the concept is guaranteed to compile
All attempts at (a) plus (c) sucked, usually taking forever to compile and/or restricting what you can check with (a). (b) was also dropped to ensure (a) was better; you can write such concept map machinery manually in many cases, but C++ doesn't provide it for you.
So, now what is it good for?
auto sum( Addable auto... values )
that uses the concept of Addable to concisely express an interface of a template. Error messages you get when passing a non-addable highlight it isn't Addable, and the expression that doesn't work.
template<class T, class A>
struct vector{
bool operator==(vector<t,A>const& o)requires EquallyComparible<T>;
};
here, we state this vector has a == if and only if the T does. Doing this before concepts is an annoying undertaking, and even adding the specs to the standard is.
This is the turing tar pit; everyting is equivalent, but nothing is easy. All programs can be written with I/O plus a (a=(a-b);(a<0)?goto c:next 3 argument instruction; but a richer language makes programs suck less. Concepts takes an esoteric branch of C++, SFINAE, and makes it clean and simpler (so more people can leverage it), and improves error messages.

Related

C++20: Validate Template Bodies Against Concepts

C++20 introduces concepts, which allows us to specify in the declaration of a template that the template parameters must provide certain capabilities. If a template is instantiated with a type that does not satisfy the constraints, compilation will fail at instantiation instead of while compiling the template's body and noticing an invalid expression after substitution.
This is great, but it begs the question: is there a way to have the compiler look at the template body, before instantiation (i.e. looking at it as a template and not a particular instantiation of a template), and check that all the expressions involving template parameters are guaranteed by the constraints to exist?
Example:
template<typename T>
concept Fooer = requires(T t)
{
{ t.foo() };
};
template<Fooer F>
void callFoo(F&& fooer)
{
fooer.foo();
}
The concept prevents me from instantiating callFoo with a type that doesn't support the expression that's inside the template body. However, if I change the function to this:
template<Fooer F>
void callFoo(F&& fooer)
{
fooer.foo();
fooer.bar();
}
This will fail if I instantiate callFoo with a type that defines foo (and therefore satisfies the constraints) but not bar. In principal, the concept should enable the compiler to look at this template and reject it before instantiation because it includes the expression fooer.bar(), which is not guaranteed by the constraint to exist.
I assume there's probably backward compatibility issues with doing this, although if this validation is only done with parameters that are constrained (not just typename/class/etc. parameters), it should only affect new code.
This could be very useful because the resulting errors could be used to guide the design of constraints. Write the template implementation, compile (with no instantiations yet), then on each error, add whatever requirement is needed to the constraint. Or, in the opposite direction, when hitting an error, adjust the implementation to use only what the constraints provide.
Do any compilers support an option to enable this type of validation, or is there a plan to add this at any point? Is it part of the specification for concepts to do this validation, now or in the future?
Do any compilers support an option to enable this type of validation, or is there a plan to add this at any point? Is it part of the specification for concepts to do this validation, now or in the future?
No, no, and no.
The feature you're looking for is called definition checking. That is, the compiler checks the definition of the template at the point of its definition based on the provided concepts, and issues errors if anything doesn't validate. This is how, for instance, Rust Traits, Swift Protocols, and Haskell Typeclasses work.
But C++ concepts don't work like that, and it seems completely infeasible to ever add support for such a thing given that C++ concepts can be arbitrary expressions rather than function signatures (as they are in other languages).
The best you can do is thoroughly unit test your templates with aggressively exotic types that meet your requirements as minimally as possible (the term here is archetype) and hope for the best.
TL;DR: no.
The design for the original C++11 concepts included validation. But when that was abandoned, the new version was designed to be much more narrow in scope. The new design was originally built on constexpr boolean conditions. The eventual requires expression was added to make these boolean checks easier to write and to bring some sanity to relationships between concepts.
But the fundamentals of the design of C++20 concepts makes it basically impossible to do full validation. Even if a concept is built entirely out of atomic requires expressions, there isn't a way to really tell if an expression is being used exactly in the code the way it is in the requires expression.
For example, consider this concept:
template<typename T, typename U>
concept func_to_u = requires(T const t)
{
{t.func()} -> std::convertible_to<U>;
};
Now, let's imagine the following template:
template<typename T, typename U> requires func_to_u<T, U>
void foo(T const &t)
{
std::optional<U> u(std::in_place, t.func());
}
If you look at std::optional, you find that the in_place_t constructor doesn't take a U. So... is this a legitimate use of that concept? After all, the concept says that code guarded by this concept will call func() and will convert the result to a U. But this template does not do this.
It instead takes the return type, instantiates a template that is not guarded by func_to_u, and that template does whatever it wants. Now, it turns out that this template does perform a conversion operation to U.
So on the one hand, it's clear that our code does conform to the intent of func_to_u. But that is only because it happened to pass the result to some other function that conformed to the func_to_u concept. But that template had no idea it was subject to the limitations of convertible_to<U>.
So... how is the compiler supposed to detect whether this is OK? The trigger condition for failure would be somewhere in optional's constructor. But that constructor is not subject to the concept; it's our outer code that is subject to the concept. So the compiler would basically have to unwind every template your code uses and apply the concept to it. Only it wouldn't even be applying the whole concept; it would just be applying the convertible_to<U> part.
The complexity of doing that quickly spirals out of control.

Recommended way to have backward compatible concepts in C++20

I'm fairly new to concepts but I like them so far and wanted to use them in a project. The problem is I also wanted the project to compile with earlier C++ standards. So far I've come up with the following pragmatized solution:
#if ISCPP20
template<NumT number = double,Index index = int,CoordinateContainer<number> coords>
#else
template<class number = double,class index = int,class coords>
#endif
where NumT, Index, and CoodinateContainer are defined concepts. The solution works but I don't like the verbosity. Is there a recommended method to introduce concepts into a codebase while not breaking backward compilation compatibility?
As a practical matter, if you want to be able to easily remove concepts from code with a macro check, you should not use any kind of compact concept syntax. That means you should always use explicit requires clauses. This makes the syntax easier to #if around.
If you have overloaded concepts, where multiple definitions exist with more restrictive concepts, that's not something that is easy to just remove. It's a part of your interface that you can pass types with different interfaces and the compiler picks which template gets instantiated through a complex overloading scheme.
So you'll have to use some form of SFINAE to do the equivalent. But exactly what you have to do depends on exactly how you're doing it. In some cases, it can be easy, such as with simple template functions that you might be able to convert to just if constexpr blocks inside a single definition.
But other cases are much more difficult. You can put requires clauses on non-template members of a template class. Doing an equivalent thing with SFINAE is much more difficult.
So how to get the same effect will depend on exactly what effect you're trying to achieve.

Why is std::pair<A,B> not the same as std::tuple<A,B>? (Is there really no way?)

Why is std::pair<A,B> not the same as std::tuple<A,B>? It always felt strange to not be able to just substitute one with the other. They are somewhat convertible, but there are limitations.
I know that std::pair<A,B> is required to have the two data members A first and B second, so it can't be just a type alias of std::tuple<A,B>. But my intuition says that we could specialize std::tuple<A,B>, that is a tuple with exactly two elements, to equal the definition of what the standard requires a std::pair to be. And then alias this to std::pair.
I guess this wouldn't be possible as it is too straight-forward to not to be already thought of, yet it wasn't done in g++'s libstdc++ for example (I didn't look at the source code of other libraries). What would the problem of this definition be? Is it "just" that it would break the standard library's binary compatibility?
You've gotta be careful about things like SFINAE and overloading. For example, the code below is currently well-formed but you would make it illegal:
void f(std::pair<int, int>);
void f(std::tuple<int, int>);
Currently, I can disambiguate between pair and tuple through overload resolution, SFINAE, template specialization, etc. These tools would all become incapable of telling them apart if you make them the same thing. This would break existing code.
There might have been an opportunity to introduce it as part of C++11, but there certainly isn't now.
This is purely historical. std::pair exist since C++98 whereas tuple came after and was initially not part of the standard.
Backward compatibility is the biggest burden for C++ evolution, preventing some nice things to be done easily !
I've not tried this and don't have the bandwidth right now to do so. You could try making a specialisation of std::tuple, derived from a sd::pair. Someone please tell me this won't work or is particularly horrible idea. I suspect you'd run into trouble with accessors.

Are there any C++ language obstacles that prevent adopting D ranges?

This is a C++ / D cross-over question. The D programming language has ranges that -in contrast to C++ libraries such as Boost.Range- are not based on iterator pairs. The official C++ Ranges Study Group seems to have been bogged down in nailing a technical specification.
Question: does the current C++11 or the upcoming C++14 Standard have any obstacles that prevent adopting D ranges -as well as a suitably rangefied version of <algorithm>- wholesale?
I don't know D or its ranges well enough, but they seem lazy and composable as well as capable of providing a superset of the STL's algorithms. Given their claim to success for D, it would seem very nice to have as a library for C++. I wonder how essential D's unique features (e.g. string mixins, uniform function call syntax) were for implementing its ranges, and whether C++ could mimic that without too much effort (e.g. C++14 constexpr seems quite similar to D compile-time function evaluation)
Note: I am seeking technical answers, not opinions whether D ranges are the right design to have as a C++ library.
I don't think there is any inherent technical limitation in C++ which would make it impossible to define a system of D-style ranges and corresponding algorithms in C++. The biggest language level problem would be that C++ range-based for-loops require that begin() and end() can be used on the ranges but assuming we would go to the length of defining a library using D-style ranges, extending range-based for-loops to deal with them seems a marginal change.
The main technical problem I have encountered when experimenting with algorithms on D-style ranges in C++ was that I couldn't make the algorithms as fast as my iterator (actually, cursor) based implementations. Of course, this could just be my algorithm implementations but I haven't seen anybody providing a reasonable set of D-style range based algorithms in C++ which I could profile against. Performance is important and the C++ standard library shall provide, at least, weakly efficient implementations of algorithms (a generic implementation of an algorithm is called weakly efficient if it is at least as fast when applied to a data structure as a custom implementation of the same algorithm using the same data structure using the same programming language). I wasn't able to create weakly efficient algorithms based on D-style ranges and my objective are actually strongly efficient algorithms (similar to weakly efficient but allowing any programming language and only assuming the same underlying hardware).
When experimenting with D-style range based algorithms I found the algorithms a lot harder to implement than iterator-based algorithms and found it necessary to deal with kludges to work around some of their limitations. Of course, not everything in the current way algorithms are specified in C++ is perfect either. A rough outline of how I want to change the algorithms and the abstractions they work with is on may STL 2.0 page. This page doesn't really deal much with ranges, however, as this is a related but somewhat different topic. I would rather envision iterator (well, really cursor) based ranges than D-style ranges but the question wasn't about that.
One technical problem all range abstractions in C++ do face is having to deal with temporary objects in a reasonable way. For example, consider this expression:
auto result = ranges::unique(ranges::sort(std::vector<int>{ read_integers() }));
In dependent of whether ranges::sort() or ranges::unique() are lazy or not, the representation of the temporary range needs to be dealt with. Merely providing a view of the source range isn't an option for either of these algorithms because the temporary object will go away at the end of the expression. One possibility could be to move the range if it comes in as r-value, requiring different result for both ranges::sort() and ranges::unique() to distinguish the cases of the actual argument being either a temporary object or an object kept alive independently. D doesn't have this particular problem because it is garbage collected and the source range would, thus, be kept alive in either case.
The above example also shows one of the problems with possibly lazy evaluated algorithm: since any type, including types which can't be spelled out otherwise, can be deduced by auto variables or templated functions, there is nothing forcing the lazy evaluation at the end of an expression. Thus, the results from the expression templates can be obtained and the algorithm isn't really executed. That is, if an l-value is passed to an algorithm, it needs to be made sure that the expression is actually evaluated to obtain the actual effect. For example, any sort() algorithm mutating the entire sequence clearly does the mutation in-place (if you want a version doesn't do it in-place just copy the container and apply the in-place version; if you only have a non-in-place version you can't avoid the extra sequence which may be an immediate problem, e.g., for gigantic sequences). Assuming it is lazy in some way the l-value access to the original sequence provides a peak into the current status which is almost certainly a bad thing. This may imply that lazy evaluation of mutating algorithms isn't such a great idea anyway.
In any case, there are some aspects of C++ which make it impossible to immediately adopt the D-sytle ranges although the same considerations also apply to other range abstractions. I'd think these considerations are, thus, somewhat out of scope for the question, too. Also, the obvious "solution" to the first of the problems (add garbage collection) is unlikely to happen. I don't know if there is a solution to the second problem in D. There may emerge a solution to the second problem (tentatively dubbed operator auto) but I'm not aware of a concrete proposal or how such a feature would actually look like.
BTW, the Ranges Study Group isn't really bogged down by any technical details. So far, we merely tried to find out what problems we are actually trying to solve and to scope out, to some extend, the solution space. Also, groups generally don't get any work done, at all! The actual work is always done by individuals, often by very few individuals. Since a major part of the work is actually designing a set of abstractions I would expect that the foundations of any results of the Ranges Study Group is done by 1 to 3 individuals who have some vision of what is needed and how it should look like.
My C++11 knowledge is much more limited than I'd like it to be, so there may be newer features which improve things that I'm not aware of yet, but there are three areas that I can think of at the moment which are at least problematic: template constraints, static if, and type introspection.
In D, a range-based function will usually have a template constraint on it indicating which type of ranges it accepts (e.g. forward range vs random-access range). For instance, here's a simplified signature for std.algorithm.sort:
auto sort(alias less = "a < b", Range)(Range r)
if(isRandomAccessRange!Range &&
hasSlicing!Range &&
hasLength!Range)
{...}
It checks that the type being passed in is a random-access range, that it can be sliced, and that it has a length property. Any type which does not satisfy those requirements will not compile with sort, and when the template constraint fails, it makes it clear to the programmer why their type won't work with sort (rather than just giving a nasty compiler error from in the middle of the templated function when it fails to compile with the given type).
Now, while that may just seem like a usability improvement over just giving a compilation error when sort fails to compile because the type doesn't have the right operations, it actually has a large impact on function overloading as well as type introspection. For instance, here are two of std.algorithm.find's overloads:
R find(alias pred = "a == b", R, E)(R haystack, E needle)
if(isInputRange!R &&
is(typeof(binaryFun!pred(haystack.front, needle)) : bool))
{...}
R1 find(alias pred = "a == b", R1, R2)(R1 haystack, R2 needle)
if(isForwardRange!R1 && isForwardRange!R2 &&
is(typeof(binaryFun!pred(haystack.front, needle.front)) : bool) &&
!isRandomAccessRange!R1)
{...}
The first one accepts a needle which is only a single element, whereas the second accepts a needle which is a forward range. The two are able to have different parameter types based purely on the template constraints and can have drastically different code internally. Without something like template constraints, you can't have templated functions which are overloaded on attributes of their arguments (as opposed to being overloaded on the specific types themselves), which makes it much harder (if not impossible) to have different implementations based on the genre of range being used (e.g. input range vs forward range) or other attributes of the types being used. Some work has been being done in this area in C++ with concepts and similar ideas, but AFAIK, C++ is still seriously lacking in the features necessary to overload templates (be they templated functions or templated types) based on the attributes of their argument types rather than specializing on specific argument types (as occurs with template specialization).
A related feature would be static if. It's the same as if, except that its condition is evaluated at compile time, and whether it's true or false will actually determine which branch is compiled in as opposed to which branch is run. It allows you to branch code based on conditions known at compile time. e.g.
static if(isDynamicArray!T)
{}
else
{}
or
static if(isRandomAccessRange!Range)
{}
else static if(isBidirectionalRange!Range)
{}
else static if(isForwardRange!Range)
{}
else static if(isInputRange!Range)
{}
else
static assert(0, Range.stringof ~ " is not a valid range!");
static if can to some extent obviate the need for template constraints, as you can essentially put the overloads for a templated function within a single function. e.g.
R find(alias pred = "a == b", R, E)(R haystack, E needle)
{
static if(isInputRange!R &&
is(typeof(binaryFun!pred(haystack.front, needle)) : bool))
{...}
else static if(isForwardRange!R1 && isForwardRange!R2 &&
is(typeof(binaryFun!pred(haystack.front, needle.front)) : bool) &&
!isRandomAccessRange!R1)
{...}
}
but that still results in nastier errors when compilation fails and actually makes it so that you can't overload the template (at least with D's implementation), because overloading is determined before the template is instantiated. So, you can use static if to specialize pieces of a template implementation, but it doesn't quite get you enough of what template constraints get you to not need template constraints (or something similar).
Rather, static if is excellent for doing stuff like specializing only a piece of your function's implementation or for making it so that a range type can properly inherit the attributes of the range type that it's wrapping. For instance, if you call std.algorithm.map on an array of integers, the resultant range can have slicing (because the source range does), whereas if you called map on a range which didn't have slicing (e.g. the ranges returned by std.algorithm.filter can't have slicing), then the resultant ranges won't have slicing. In order to do that, map uses static if to compile in opSlice only when the source range supports it. Currently, map 's code that does this looks like
static if (hasSlicing!R)
{
static if (is(typeof(_input[ulong.max .. ulong.max])))
private alias opSlice_t = ulong;
else
private alias opSlice_t = uint;
static if (hasLength!R)
{
auto opSlice(opSlice_t low, opSlice_t high)
{
return typeof(this)(_input[low .. high]);
}
}
else static if (is(typeof(_input[opSlice_t.max .. $])))
{
struct DollarToken{}
enum opDollar = DollarToken.init;
auto opSlice(opSlice_t low, DollarToken)
{
return typeof(this)(_input[low .. $]);
}
auto opSlice(opSlice_t low, opSlice_t high)
{
return this[low .. $].take(high - low);
}
}
}
This is code in the type definition of map's return type, and whether that code is compiled in or not depends entirely on the results of the static ifs, none of which could be replaced with template specializations based on specific types without having to write a new specialized template for map for every new type that you use with it (which obviously isn't tenable). In order to compile in code based on attributes of types rather than with specific types, you really need something like static if (which C++ does not currently have).
The third major item which C++ is lacking (and which I've more or less touched on throughout) is type introspection. The fact that you can do something like is(typeof(binaryFun!pred(haystack.front, needle)) : bool) or isForwardRange!Range is crucial. Without the ability to check whether a particular type has a particular set of attributes or that a particular piece of code compiles, you can't even write the conditions which template constraints and static if use. For instance, std.range.isInputRange looks something like this
template isInputRange(R)
{
enum bool isInputRange = is(typeof(
{
R r = void; // can define a range object
if (r.empty) {} // can test for empty
r.popFront(); // can invoke popFront()
auto h = r.front; // can get the front of the range
}));
}
It checks that a particular piece of code compiles for the given type. If it does, then that type can be used as an input range. If it doesn't, then it can't. AFAIK, it's impossible to do anything even vaguely like this in C++. But to sanely implement ranges, you really need to be able to do stuff like have isInputRange or test whether a particular type compiles with sort - is(typeof(sort(myRange))). Without that, you can't specialize implementations based on what types of operations a particular range supports, you can't properly forward the attributes of a range when wrapping it (and range functions wrap their arguments in new ranges all the time), and you can't even properly protect your function against being compiled with types which won't work with it. And, of course, the results of static if and template constraints also affect the type introspection (as they affect what will and won't compile), so the three features are very much interconnected.
Really, the main reasons that ranges don't work very well in C++ are the some reasons that metaprogramming in C++ is primitive in comparison to metaprogramming in D. AFAIK, there's no reason that these features (or similar ones) couldn't be added to C++ and fix the problem, but until C++ has metaprogramming capabilities similar to those of D, ranges in C++ are going to be seriously impaired.
Other features such as mixins and Uniform Function Call Syntax would also help, but they're nowhere near as fundamental. Mixins would help primarily with reducing code duplication, and UFCS helps primarily with making it so that generic code can just call all functions as if they were member functions so that if a type happens to define a particular function (e.g. find) then that would be used instead of the more general, free function version (and the code still works if no such member function is declared, because then the free function is used). UFCS is not fundamentally required, and you could even go the opposite direction and favor free functions for everything (like C++11 did with begin and end), though to do that well, it essentially requires that the free functions be able to test for the existence of the member function and then call the member function internally rather than using their own implementations. So, again you need type introspection along with static if and/or template constraints.
As much as I love ranges, at this point, I've pretty much given up on attempting to do anything with them in C++, because the features to make them sane just aren't there. But if other folks can figure out how to do it, all the more power to them. Regardless of ranges though, I'd love to see C++ gain features such as template constraints, static if, and type introspection, because without them, metaprogramming is way less pleasant, to the point that while I do it all the time in D, I almost never do it in C++.

Restricting std::sort to random access iterators

I was just wondering, since you can only pass random access iterators to std::sort anyway, why not enforce that restriction by defining it only for random access iterators in the first place?
#include <iterator>
#include <type_traits>
template <typename ForwardIterator>
typename std::enable_if<
std::is_same<
typename std::iterator_traits<ForwardIterator>::iterator_category,
std::random_access_iterator_tag>::value,
void>
::type sort(ForwardIterator begin, ForwardIterator end)
{
// ...
}
I find a single line error message a lot easier to read than pages and pages of error messages resulting from type errors far down in the implementation.
You could do the same with other algorithms. The standard C++ core language has always been expressive enough for that task, right? So, any particular reason why this was not done?
The core language has always been expressive enough to handle such checks, but when the first standard was being prepared (around 1996/1997), the tricks that you can play with SFINAE (what enable_if is based upon) were not yet known and the support for advanced template wizardry was limited in the compilers.
So, the reason why the standard did not mandate it was because the needed techniques were not invented yet.
The reason why compiler/library writers did not add it after the fact is probably just plain economics: not enough people asked for the feature, and when people did start asking for better diagnostics, hope was on the concepts proposal to take care of it. Unfortunately, this proved to be a bit too hard to get finalised in time.
My guess is that SFINAE was invented (or discovered) after the standard library implementations had reached a certain maturity. After that, changes to the core library had to be very justified in order to prevent the introduction of regressions and I guess that mere cosmetics are somewhat hard to justify.
That said, the GCC for example already does have a lot of diagnostics for template-related error messages, e.g. macros that perform a kind of concept checking. For example, the GCC-libstdc++ has the following:
// concept requirements
__glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<
_RandomAccessIterator>)
__glibcxx_function_requires(_LessThanComparableConcept<_ValueType>)
__glibcxx_requires_valid_range(__first, __last);
Actually, when there is only one overload of an algorithm, you'll nearly always get better diagnostics by causing a compilation error just inside using something like Boost.ConceptCheck or __glibcxx_function_requires. When SFINAE (which is what enable_if uses) leaves you with an empty overload set, most compilers simply tell you "there's no matching function," which tends not to be very helpful.
One of the nice things about templates in C++ is they can have a sort of static 'duck typing'. I can't speak for this particular case, but in many templates, as long as you keep the interface the same, all the hierarchy nonsense doesn't matter. And that's a good thing.