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: members x, y (and potentially some operations on a point, but that is not the point here)

  • ⟶ Turn hypotenuse() into a template

  • Straightforward

  • ⟶ 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 required

  • Still 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>;
      |     ~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~