Before the acceptance of Concepts Lite TS into C++17, there was a lot of effort out there in the community to make a C++11/14 compatible implementation of Concepts, at least an emulation wrapping the usual SFINAE tricks.
Concepts provide nice syntax for concept definition type constraining. No more SFINAE! (I hope). This will make C++ syntax clean in one of the very few points I think C++ still needs a “lifting”: templates.
Concepts Lite also defines the hierarchical organization of different type concepts that have always been with us, but only as documentation notes.
Previous work
In this widespread C++ community, taking template meta-programming seriously introduces you into a very little niche of people as-crazy-as-you, currently leaded by what I call “The Gang Of Tmp”: Louis Dionne, Paul Fultz II, and Eric Niebler. There are other players that are rising and I would like to see more of their work in the future, like Peter Dimov’s series on Simple C++11 Meta-programming, and the amazing work from Filip Roséen on stateful meta-programming.
Anyone else noticed the fun fact that each one of the gang above has implemented his own version of a C++11/14 Concepts library? Fultz has Tick, Boost.Hana has another one built in its internals, Niebler’s range-v3 proposal is based heavily on Concept specification. We C++ers really have a serious problem on code sharing and dependency management…
I have tried all those libs. Tick provides straightforward trait specification and checking, the best alternative for defining traits and concepts in your libraries these days: Just clone Tick into your include directories and you are ready. No dependencies, no headaches. I like range-v3 concepts since Niebler did a lot of work providing a wide set of concepts for the library. Almost any Standard Library concept is already defined there, plus the ones about ranges. Finally is Hana from Dionne: You really guys should try Boost.Hana. Hana is C++ The Right Way. Seriously. Concepts implementation in Hana are a mere implementation detail, but I’m confident Louis is currently working on modularizing the library, so it could be available as a standalone module in the future.
The problem
One of the greatest gifts given by the Concepts Lite proposal is to reduce template diagnostic boilerplate when a concept is not satisfied: Instead of showing the whole “call stack” of instantiation failures, Concepts Lite will provide a meaningful message at the uppermost level:
Compare that to its SFINAE based alternative:
clang at least identifies this pattern and gives you something similar to “Specialization disabled by enable_if”.
So far so good. But consider a more complex concept, one that’s the aggregation of multiple properties and/or refines some other concepts. Take for example TotallyOrdered
concept from range-v3:
CONCEPT_REQUIRES()
macro will tell you that foo
does not satisfy the TotallyOrdered
concept, but not why. This issue was already noticed in the recent addition of Concepts Lite branch into GCC trunk.
I faced this problem writing a custom range type while playing with range-v3:
I might have made a mistake with the Iterator
class since the internal concept checks that range-v3 has said that MyRange
was not a range, so I couldn’t apply a view on it. MyRange
doesn’t satisfy the “Range
” concept. But why? I had to deep dive into the range-v3 concepts hierarchy sources to figure out what was wrong with my iterators.
When a C++ programmer is frustrated on X, writes yet another X library
That experience left me a bad taste in the mouth. That wasn’t fine, Niebler took a lot of work to get the concepts right, every function and datatype from range-v3 is constrained on the types it works with. But a frustrated user could just throw away range-v3 at first chance if he is not able to figure out the problems that may arise.
The C++11/14 concepts implementations described above are focused on success, on passing the type across all the concept properties to look if those are fulfilled, grabbing out the result for SFINAEing functions, classes, etc. Those concepts are just functions computing a result, whether the concept was satisfied by a given type or not.
But what if we design concepts that are not mere sets of properties that should be true, but a bag of meta-information about this properties?
Since the properties are formulated and checked at compile time, there should be a way to, when checking if a type T
meets a concept C
, collect enough information to give a meaningful message to the user about what was wrong. That is, instead of just carrying boolean information about the application of T
in C
, also store information about how T
acted at each property of C
.
All the guys above have their own concepts checking library! I have to write mine too…
Worm
Worm is the codename of my user friendly concepts library. I’m open for name suggestions.
It is focused on defining concepts in a declarative way, plus storing information about the requirements of the concept applied to a type.
That’s the Allocatable
concept directly translated from range-v3 sources. The preprocessor macros make the code look like ugly COBOL, but hide a lot of sorcery to the user.
Any worm concept has a ::value
boolean member with the result of the instantiation of the concept on a given type or types. Is the member that type traits usually have, the one you use for SFINAE and related things. But in addition, this concepts have a message
member, a constexpr
string with detailed information about the instantiation of the concept.
If you print that string, you will see how exactly the type behaved in the concept:
Defining a concept
Worm concepts start with a “concept block”, a pair of BEGIN_CONCEPT()
END_CONCEPT()
macros with the name of the concept as parameter.
BEGIN_CONCEPT()
also supports additional extra parameters that are the existing concepts that may be refined by our concept. For example, this is the translation of Regular
concept from range-v3:
Worm concepts can take any number of types as parameters, and those are exposed in multiple ways to make the concept the most readable possible on each scenario:
-
First parameter:
T
,First
,Lhs
, orHead
. For optimal readability in unary, binary, and n-ary concepts. Concepts without parameters are not valid, so the first parameter is always defined. -
Second Parameter:
U
,Second
,Rhs
. If the concept was instanced with one parameter only, its value is undefined. -
Rest:
Tail
, a variadic pack packing all parameters except first.Head
andTail
are suited for working with recursive concepts in an easy way. If there is one parameter only, the pack is empty. -
All:
Ts
, a variadic pack.
As an example, imagine you want to write an n-ary equivalent of Regular
, a concept that checks if all the types passed are Regular
:
There’s no awkward syntax, just simple variadic pack expansion. Let’s see the message
from an instance of Regulars
:
Requirements
Now that we have learned how to declare a new concept and make it refine one or more existing ones, it’s time to write the requirements for our concept.
Requirements are defined just inside the concept block. For example:
That’s the REQUIRES_EXPR()
requirement, which checks if an expression is valid. In this case, it checks if the type has a member function foo()
. All the type parameters are accessible by requirements, but these have an extra underscore at the end. Matters of sorcery limitations…
Any number of requirements is supported inside a concept.
Currently worm provides the following requirements:
REQUIRES_EXPR(<expression>)
: Checks if<expression>
is valid.REQUIRES_EXPR_EXPECTED(<expression>, <expected>)
: Checks if<expression>
is valid and yields a value of type<expected>
.REQUIRES_EXPR_CONVERTIBLE(<expression>, <expected>)
: Checks if<expression>
is valid and yields a value convertible to type<expected>
.REQUIRES_TRAIT_EXPECTED(<trait instance>, <expected>)
: Checks if a given type-trait/concept instance yields the value (::value
)<expected>
.REQUIRES_TRAIT(<trait instance>)
: Checks if a given type-trait/concept instance is fulfilled. Just an alias ofREQUIRES_TRAIT_EXPECTED(<trait instance>, true)
.
Porting existing type-traits and concepts
Wrapping Standard Library type traits is a tiresome task, so worm also provides a simple utility CONCEPT_FROM_TRAIT(<name>, <trait>)
to help with it.
CONCEPT_FROM_TRAIT()
is a simple macro defined as:
Had you noticed the Ts...
in the Regulars
message above? That’s why all the Standard Library concepts were defined from their equivalent type traits using this macro:
Future work
Ignoring the format of the messages, which I’m still not fully satisfied with, there’s a point that bothers me: The message doesn’t show the values of the type parameters. The reason is simple: When I started with worm, I had no way to take a constexpr
string with the name of a given type.
That’s why I have been working on an independent project, ctti. ctti provides compile-time type information similar to what std::type_info
and std::type_index
give through RTTI, but at compile time. We have ctti working on GCC, Clang, and Visual Studio 2015, so I hope I could use it in worm in a couple of weeks. This is the kind of message I’m pursuing:
Next posts
Worm was not released yet, I would like to explain how it works in detail before sharing it with the community. It’s all about macros and tricks pushing compiler limits, so I will not show you the codez until I’m sure you (and me xD) understand how it works.