Sandor Dargo
Posted on March 17, 2021
Last week we started to discuss how to write our own concepts. Our first step was to combine different already existing concepts, then we continued with declaring constraints on the existence of certain operations, certain methods.
Today, we are going to discover how to express our requirements on function return types, how to write type requirements (and what they are) and we are going to finish with discussing nested requirements.
Write your own constraints
Last time, we had an example with the concept HasSquare
. It accepts any type that has a square
function regardless of the return type.
#include <iostream>
#include <string>
#include <concepts>
template <typename T>
concept HasSquare = requires (T t) {
t.square();
};
class IntWithoutSquare {
public:
IntWithoutSquare(int num) : m_num(num) {}
private:
int m_num;
};
class IntWithSquare {
public:
IntWithSquare(int num) : m_num(num) {}
int square() {
return m_num * m_num;
}
private:
int m_num;
};
void printSquare(HasSquare auto number) {
std::cout << number.square() << '\n';
}
int main() {
printSquare(IntWithoutSquare{4}); // error: use of function 'void printSquare(auto:11) [with auto:11 = IntWithoutSquare]' with unsatisfied constraints,
// the required expression 't.square()' is invalid
printSquare(IntWithSquare{5});
}
Now let's continue with constraining the return types.
Requirements on return types (a.k.a compound requirements)
We've seen how to write a requirement expressing the need of a certain API, a certain function.
But did we also constrain the return type of those functions?
No, we didn't. IntWithSquare
satisfies the HasSquare
concept both with int square()
and void square()
.
If you want to specify the return type, you must use something that is called a compound requirement.
Here is an example:
template <typename T>
concept HasSquare = requires (T t) {
{t.square()} -> std::convertible_to<int>;
};
Notice the following:
- The expression on what you want to set a return type requirement must be surrounded by braces (
{}
), then comes an arrow (->
) followed by the constraint of the return type. - A constraint cannot simply be a type. Had you written simply
int
, you would receive an error message: return-type-requirement is not a type-constraint. The original concepts TS allowed the direct usage of types, so if you experimented with that, you might be surprised by this error. This possibility was removed by P1452R2.
There are a number of reasons for this removal. One of the motivations was that it would interfere with a future direction of wanting to adopt a generalized form of auto
, like vector<auto>
or vector<Concept>.
So instead of simply naming a type you have to choose a concept! If you want to set the return type one of the two following options will satisfy your needs:
{t.square()} -> std::same_as<int>;
{t.square()} -> std::convertible_to<int>;
I think that the difference is obvious. In case of std::same_as
, the return value must be the same as specified as the template argument, while with std::convertible_to
conversions are allowed.
In order to demonstrate this, let's have a look at the following example:
#include <iostream>
#include <concepts>
template <typename T>
concept HasIntSquare = requires (T t) {
{t.square()} -> std::same_as<int>;
};
template <typename T>
concept HasConvertibleToIntSquare = requires (T t) {
{t.square()} -> std::convertible_to<int>;
};
class IntWithIntSquare {
public:
IntWithIntSquare(int num) : m_num(num) {}
int square() const {
return m_num * m_num;
}
private:
int m_num;
};
class IntWithLongSquare {
public:
IntWithLongSquare(int num) : m_num(num) {}
long square() const {
return m_num * m_num;
}
private:
int m_num;
};
class IntWithVoidSquare {
public:
IntWithVoidSquare(int num) : m_num(num) {}
void square() const {
std::cout << m_num * m_num << '\n';
}
private:
int m_num;
};
void printSquareSame(HasIntSquare auto number) {
std::cout << number.square() << '\n';
}
void printSquareConvertible(HasConvertibleToIntSquare auto number) {
std::cout << number.square() << '\n';
}
int main() {
printSquareSame(IntWithIntSquare{1}); // int same as int
// printSquareSame(IntWithLongSquare{2}); // long not same as int
// printSquareSame(IntWithVoidSquare{3}); // void not same as int
printSquareConvertible(IntWithIntSquare{4}); // int convertible to int
printSquareConvertible(IntWithLongSquare{5}); // int convertible to int
// printSquareConvertible(IntWithVoidSquare{6}); // void not convertible to int
}
/*
1
16
25
*/
In the above example, we can observe that the class with void square() const
doesn't satisfy either the HasIntSquare
or the HasConvertibleToIntSquare
concepts.
IntWithLongSquare
, so the class with the function long square() const
doesn't satisfy the concept HasIntSquare
as long is not the same as int
, but it does satisfy the HasConvertibleToIntSquare
concept as long
is convertible to int
.
Class IntWithIntSquare
satisfies both concepts as an int
is obviously the same as int
and it's also convertible to an int
.
Type requirements
With type requirements, we can express that a certain type is valid in a specific context. Type requirements can be used to verify that
- a certain nested type exists
- a class template specialization names a type
- an alias template specialization names a type
You have to use the keyword typename
along with the type name that is expected to exist:
template<typename T>
concept TypeRequirement = requires {
typename T::value_type;
typename Other<T>;
};
The concept TypeRequirement
requires that the type T
has a nested type value_type
, and that the class template Other
can be instantiated with T
.
Let's see how it works:
#include <iostream>
#include <vector>
template <typename>
struct Other;
template<typename T>
concept TypeRequirement = requires {
typename T::value_type;
typename Other<T>;
};
int main() {
TypeRequirement auto myVec = std::vector<int>{1, 2, 3};
// TypeRequirement auto myInt {3}; // error: deduced initializer does not satisfy placeholder constraints ... the required type 'typename T::value_type' is invalid
}
The expression TypeRequirement auto myVec = std::vector<int>{1, 2, 3}
(line 13) is valid.
A std::vector
has an inner member type value_type
(requested on line 8) and the class template Other
can be instantiated with std::vector<int>
(line 9).
At the same time, an int
doesn't have any member, in particular value_type
, so it doesn't satisfy the constraints of TypeRequirement
.
Let's change class template Other
and make a requirement on the template parameter by making sure that Other
cannot be instantiated with a vector
of int
s.
template <typename T>
requires (!std::same_as<T, std::vector<int>>)
struct Other
Now, the line TypeRequirement auto myVec = std::vector<int>{1, 2, 3};
fails with the following error message:
main.cpp: In function 'int main()':
main.cpp:16:55: error: deduced initializer does not satisfy placeholder constraints
16 | TypeRequirement auto myVec = std::vector<int>{1, 2, 3};
| ^
main.cpp:16:55: note: constraints not satisfied
main.cpp:10:9: required for the satisfaction of 'TypeRequirement<std::vector<int, std::allocator<int> > >'
main.cpp:10:27: in requirements [with T = std::vector<int, std::allocator<int> >]
main.cpp:12:12: note: the required type 'Other<T>' is invalid
12 | typename Other<T>;
| ~~~~~~~~~^~~~~~~~~
cc1plus: note: set '-fconcepts-diagnostics-depth=' to at least 2 for more detail
With type requirements, we can make sure that a class has a nested member type or that a template specialization is possible.
To show that a concept can be used to prove that an alias template specialization names a type, let's take our original example and create a template alias Reference
:
template<typename T> using Reference = T&;
And use it in the concept TypeRequirement
:
template<typename T>
concept TypeRequirement = requires {
typename T::value_type;
typename Other<T>;
typename Reference<T>;
};
Our example should still compile:
#include <iostream>
#include <vector>
template <typename>
struct Other;
template<typename T> using Reference = T&;
template<typename T>
concept TypeRequirement = requires {
typename T::value_type;
typename Other<T>;
typename Reference<T>;
};
int main() {
TypeRequirement auto myVec = std::vector<int>{1, 2, 3};
}
Nested requirements
We can use nested requirements to specify additional constraints in a concept without introducing another named concepts.
You can think of nested requirements as one would think about lambda functions for STL algorithms. You can use lambdas to alter the behaviour of an algorithm without the need of naming a function or a function object.
In this case, you can write a constraint more suitable for your needs without the need of naming one more constraint that you'd only use in one (nested) context.
Its syntax follows the following form:
requires constraint-expression;
Let's start with a simpler example. Where the concept Coupe
uses two other concepts Car
and Convertible
.
#include <iostream>
struct AwesomeCabrio {
void openRoof(){}
void startEngine(){}
};
struct CoolCoupe {
void startEngine(){}
};
template<typename C>
concept Car = requires (C car) {
car.startEngine();
};
template<typename C>
concept Convertible = Car<C> && requires (C car) {
car.openRoof();
};
template<typename C>
concept Coupe = Car<C> && requires (C car) {
requires !Convertible<C>;
};
int main() {
Convertible auto cabrio = AwesomeCabrio{};
//Coupe auto notACoupe = AwesomeCabrio{}; // nested requirement '! Convertible<C>' is not satisfied
Coupe auto coupe = CoolCoupe{};
}
Let's have a look at the concept Coupe
. First, we make sure that only types satisfying the Car
concept are accepted. Then we introduce a nested concept that requires that our template type is not a Convertible
.
It's true that we don't need the nested constraint, we could express ourselves without it:
template<typename C>
concept Coupe = Car<C> && !Convertible<C>;
Nevertheless, we saw the syntax in a working example.
Nested requires clauses can be used more effectively with local parameters that are listed in the outer requires
scope, like in the next example with C clonable
:
#include <iostream>
struct Droid {
Droid clone(){
return Droid{};
}
};
struct DroidV2 {
Droid clones(){
return Droid{};
}
};
template<typename C>
concept Clonable = requires (C clonable) {
clonable.clone();
requires std::same_as<C, decltype(clonable.clone())>;
};
int main() {
Clonable auto c = Droid{};
// Clonable auto c2 = DroidV2{}; // nested requirement 'same_as<C, decltype (clonable.clone())>' is not satisfied
}
In this example, we have two droid types, Droid
and DroidV2
. We expect that droids should be clonable meaning that each type should have a clone method that returns another droid of the same type. With DroidV2
we made a mistake and it still returns Droid
.
Can we write a concept that catches this error?
We can, in fact as probably you noticed, we already did. In the concept Clonable
we work with a C cloneable
local parameter. With the nested requirement requires std::same_as<C, decltype(clonable.clone())>
we express that the clone method should return the same type as the parameters'.
You might argue that there is another way to express this, without the nested clause and you'd be right:
template<typename C>
concept Clonable = requires (C clonable) {
{ clonable.clone() } -> std::same_as<C>;
};
For a more complex example, I'd recommend you to check the implementation of SemiRegular
concepts on C++ Reference.
To incorporate one of the requirements of Semiregular
to our Clonable
concept, we could write this:
template<typename C>
concept Clonable = requires (C clonable) {
{ clonable.clone() } -> std::same_as<C>;
requires std::same_as<C*, decltype(&clonable)>;
};
This additional line makes sure that the address of operator (&
) returns the same type for the cloneable
parameter as C*
is.
I agree, it doesn't make much sense in this context (it does for SemiRegular
), but it's finally an example that is not easier to express without a nested requirement than with.
In the next post, we'll see how to use a nested requirement when even the enclosing concept is unnamed.
Conclusion
Today we continued and finished discussing what building blocks are available for us to write our own concepts. We saw how to make constraints on function return types, how to use type requirements on inner types, template aliases and specialisations and finally we saw that it's possible to nest requirements, even though often there are easier ways to express ourselves.
Next time, we'll continue with some real life examples of how concepts can make our projects easier to understand. Stay tuned!
If you want to learn more details about C++ concepts, check out my book on Leanpub!
Connect deeper
If you liked this article, please
- hit on the like button,
- subscribe to my newsletter
- and let's connect on Twitter!
Posted on March 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.