Writing Your Own Concepts

Starting Point: Good Old Function

  • N-dimensional hypotenuse

  • std::vector<double>

  • Using index based iteration, only too keep requirements to a minimum

  • Requirements

    • .size()

    • .operator[]()

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);
}

Need Template

  • point2d: members x, y (and potentially some operations on a point, but that is not the point here)

  • How to fit that into hypotenuse()?

  • ⟶ implement what’s required by hypotenuse() (.size() and .operator[]())

  • ⟶ Turn hypotenuse() into a template

  • Straightforward

class point2d
{
public:
    point2d(double x, double y)
    : _x{x}, _y{y} {}

    size_t size() const { return 2; }                  // <--- point2d has .size() too
    double operator[](size_t i) const                  // <--- point2d has [] too
    {
        if (i==0) return _x;
        if (i==1) return _y;
        return 666;
    }
private:
    double _x, _y;
};

Error: Requirement Not Fulfilled

class point2d
{
    // size_t size() const { return 2; }               // <--- requirement *not* fulfilled
};
  • Uncomment .size()

  • Error message is relatively clear (see below)

  • error: ‘const class point2d’ has no member named ‘size’

  • Can be worse though; for example if helper types (possibly nested a dozen levels deep) are instantiated by the implementation

example-3-requirement-not-fulfilled.cpp: In instantiation of ‘double hypotenuse(const V&) [with V = point2d]’:
example-3-requirement-not-fulfilled.cpp:39:28:   required from here
example-3-requirement-not-fulfilled.cpp:9:26: error: ‘const class point2d’ has no member named ‘size’
    9 |     for (size_t i=0; i<v.size(); ++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

template <typename V>
concept has_size = requires(V v) {
    v.size();                                          // <--- compiles
};
  • Concept usage: good old full explicit template

template <typename V>
requires has_size<V>
double hypotenuse(const V& v) { /*...*/ }
  • Concept usage: good old full explicit template (after declaration)

template <typename V>
double hypotenuse(const V& v) requires has_size<V> { /*...*/ }
  • Concept usage: abbreviated function templates

double hypotenuse(const has_size auto& v) { /*...*/ }

Concept: index_returns_double

  • Hmm … what if elements are not double?

  • ⟶ Somebody could use hypotenuse() on something that has int coordinates

  • Could we check this?

Ruin the whole thing …

  • Modify object initialization to take real double values, e.g. {3.5L, 4.5L}

  • ⟶ Result not straight 5 anymore

  • Modify point2d::operator[]() to return int

  • ⟶ Result straight 5 again

Concept index_returns_double

  • First use std::same_as<double>

  • ⟶ Constraint check fails: std::vector::operator[]() is not same as double (rather, it returns double&)

  • Solution: std::commone_reference_with<double>

template <typename V>
concept index_returns_double = requires(V v) {
    { v[0] } -> std::common_reference_with<double>;
};
  • Argh: checking multiple constraints is not possible with abbreviated function templates

  • ⟶ fall back to ordinary template syntax, and its requires clause