Writing Your Own Concepts¶
Starting Point: Good Old Function¶
N-dimensional hypotenuse, over
std::vector<double>
Uses
v.size()
v.operator[]()
#include <vector>
#include <cmath>
#include <iostream>
double hypotenuse(const std::vector<double>& v)
{
double sumsq = 0;
for (size_t i=0; i<v.size(); ++i) // <--- vector has .size()
sumsq += v[i]*v[i]; // <--- vector has []
return std::sqrt(sumsq);
}
int main()
{
std::vector<double> v = {3,4};
std::cout << hypotenuse(v) << std::endl;
}
Problem: std::vector<double>
Is Not Enough ⟶ Template¶
point2d
: membersx, y
(and potentially some operations on a point, but that is not the point here)⟶ Turn
hypotenuse()
into a templateStraightforward
⟶ Errors deep down in the implementation
#include <vector>
#include <cmath>
#include <iostream>
template <typename V>
double hypotenuse(const V& v)
{
double sumsq = 0;
for (size_t i=0; i<v.size(); ++i)
sumsq += v[i]*v[i];
return std::sqrt(sumsq);
}
class point2d
{
public:
point2d(double x, double y) : _x{x}, _y{y} {}
private:
double _x, _y;
};
int main()
{
point2d p{3,4};
std::cout << hypotenuse(p) << std::endl;
return 0;
}
error: ‘const class point2d’ has no member named ‘size’
9 | for (size_t i=0; i<v.size(); ++i)
| ~~^~~~
error: no match for ‘operator[]’ (operand types are ‘const point2d’ and ‘size_t’ {aka ‘long unsigned int’})
10 | sumsq += v[i]*v[i];
| ~^
error: no match for ‘operator[]’ (operand types are ‘const point2d’ and ‘size_t’ {aka ‘long unsigned int’})
10 | sumsq += v[i]*v[i];
| ~^
Concept: has_size
¶
Implement concept
has_size
Requires that any object of
V
has a.size()
method⟶ i.e. the expression
v.size()
compiles
#include <vector>
#include <cmath>
#include <iostream>
template <typename V>
concept has_size = requires(V v) {
v.size(); // <-- ok if this compiles
};
template <typename V>
double hypotenuse(const V& v)
requires has_size<V>
{
double sumsq = 0;
for (size_t i=0; i<v.size(); ++i)
sumsq += v[i]*v[i];
return std::sqrt(sumsq);
}
class point2d
{
public:
point2d(double x, double y) : _x{x}, _y{y} {}
private:
double _x, _y;
};
int main()
{
point2d p{3,4};
std::cout << hypotenuse(p) << std::endl;
return 0;
}
error: no matching function for call to ‘hypotenuse(point2d&)’
31 | std::cout << hypotenuse(p) << std::endl;
| ~~~~~~~~~~^~~
... blah ...
note: constraints not satisfied
... blah ...
required for the satisfaction of ‘has_size<V>’ [with V = point2d]
Fix has_size
Failure In point2d
¶
Fix: add
point2d::size()
as requiredStill old-school-complains about
operator[]
#include <vector>
#include <cmath>
#include <iostream>
template <typename V>
concept has_size = requires(V v) {
v.size(); // <-- ok if this compiles
};
template <typename V>
double hypotenuse(const V& v)
requires has_size<V>
{
double sumsq = 0;
for (size_t i=0; i<v.size(); ++i)
sumsq += v[i]*v[i];
return std::sqrt(sumsq);
}
class point2d
{
public:
point2d(double x, double y) : _x{x}, _y{y} {}
size_t size() const { return 2; } // <--- satisfies requirement
private:
double _x, _y;
};
int main()
{
point2d p{3,4};
std::cout << hypotenuse(p) << std::endl;
return 0;
}
Problem: What If I Pass Elements That Do Not Support *
?¶
E.g.:
std::vector<std::string>
Which is nonsense because vector (in its math sense) coordinates must be multipliable
⟶ does not support
operator*()
#include <string>
#include <vector>
#include <cmath>
#include <iostream>
template <typename V>
concept has_size = requires(V v) {
v.size();
};
template <typename V>
double hypotenuse(const V& v)
requires has_size<V>
{
double sumsq = 0;
for (size_t i=0; i<v.size(); ++i)
sumsq += v[i]*v[i];
return std::sqrt(sumsq);
}
int main()
{
std::vector v{"eins", "zwei"}; // <-- std::string has no ``*``
std::cout << hypotenuse(v) << std::endl;
return 0;
}
error: no match for ‘operator*’ (operand types are ‘const __gnu_cxx::__alloc_traits<std::allocator<std::pair<int, int> >, std::pair<int, int> >::value_type’ {aka ‘const std::pair<int, int>’} and ‘const __gnu_cxx::__alloc_traits<std::allocator<std::pair<int, int> >, std::pair<int, int> >::value_type’ {aka ‘const std::pair<int, int>’})
17 | sumsq += v[i]*v[i];
| ~~~~^~~
Concept: coordinate_is_multipliable
¶
Again, require this explicitly
⟶ result of
operator[]
must support multiplication⟶ this is exactly how the concept looks
#include <utility>
#include <vector>
#include <cmath>
#include <iostream>
template <typename V>
concept has_size = requires(V v) {
v.size();
};
template <typename V>
concept coordinate_is_multipliable = requires(V v) {
v[0]*v[0]; // <-- as a requirement, this must compile
};
template <typename V>
double hypotenuse(const V& v)
requires
has_size<V>
&& // <-- combining requirements: &&
coordinate_is_multipliable<V>
{
double sumsq = 0;
for (size_t i=0; i<v.size(); ++i)
sumsq += v[i]*v[i];
return std::sqrt(sumsq);
}
int main()
{
std::vector v{"eins", "zwei"};
std::cout << hypotenuse(v) << std::endl;
return 0;
}
note: constraints not satisfied
In substitution of ‘template<class V> double hypotenuse(const V&) requires (has_size<V>) && (coordinate_is_multipliable<V>) [with V = std::vector<std::pair<int, int> >]’:
required from here
required for the satisfaction of ‘coordinate_is_multipliable<V>’ [with V = std::vector<std::pair<int, int>, std::allocator<std::pair<int, int> > >]
in requirements with ‘V v’ [with V = std::vector<std::pair<int, int>, std::allocator<std::pair<int, int> > >]
note: the required expression ‘(v[0] * v[0])’ is invalid
13 | v[0]*v[0]; // <-- as a requirement, this must compile
| ~~~~^~~
Concept: Require Coordinate Type To Be Double¶
Rationale: because I can 💪
There is no real reason but to show another syntactical highlight
… and some usage of
<concept>
#include <vector>
#include <cmath>
#include <iostream>
template <typename V>
concept contains_double_likes = requires(V v)
{
{ v[0] } -> std::same_as<double>;
};
template <typename V>
double hypotenuse(const V& v)
requires contains_double_likes<V>
{
double sumsq = 0;
for (size_t i=0; i<v.size(); ++i)
sumsq += v[i]*v[i];
return std::sqrt(sumsq);
}
int main()
{
std::vector v{"eins", "zwei"};
std::cout << hypotenuse(v) << std::endl;
return 0;
}
note: constraints not satisfied
In substitution of ‘template<class V> double hypotenuse(const V&) requires contains_double_likes<V> [with V = std::vector<const char*, std::allocator<const char*> >]’:
required from here
required for the satisfaction of ‘contains_double_likes<V>’ [with V = std::vector<const char*, std::allocator<const char*> >]
in requirements with ‘V v’ [with V = std::vector<const char*, std::allocator<const char*> >]
note: ‘v[0]’ does not satisfy return-type-requirement
9 | { v[0] } -> std::same_as<double>;
| ~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~