Skip to content

Session: Concepts

David Heller edited this page Mar 16, 2017 · 5 revisions

Reading Material

Other stuff:

Code example and tasks

In the following we will specialize the operation of doubling a number. In the general case we will want to use multiplication by two, but for integral types we want to use bit shifting. We also want to check how programming errors are shown to the user, because it is an important indicator of how easy a solution is (in addition to code complexity).

via inheritance

Code (click to expand)

#include <iostream>
#include <type_traits>

class Number
{
public:
    double val;
    Number() {}
    Number(double in) : val{in} {}
    virtual Number doubleTheValue() const
    {
        std::cerr << "general funcion!\n";
        return val * 2;
    }

    operator double() const
    {
        return val;
    }

};

class DoubleNumber : public Number
{
public:
    DoubleNumber(double in) : Number(in) {}
};

class IntNumber : public Number
{
public:
    int val;
    IntNumber(int in) : val{in} {}
    virtual Number doubleTheValue() const
    {
        std::cerr << "special funcion!\n";
        return (val << 1);
    }
    operator int() const
    {
        return val;
    }
};

void print2(Number const & v)
{
    std::cout << v.doubleTheValue() << '\n';
}

int main()
{
//    print2("foo");
    print2(DoubleNumber{0.2});
    print2(IntNumber{2});

    return 0;
}

Pro:

  • it works
  • if we uncomment print2("foo"); we immediately get a readable error in the decleration of print2:
doubleTheValue_object.cpp: In function 'int main()':
doubleTheValue_object.cpp:52:17: error: invalid initialization of reference of type 'const Number&' from expression of type 'const char [4]'
     print2("foo");
                 ^
doubleTheValue_object.cpp:45:6: note: in passing argument 1 of 'void print2(const Number&)'
 void print2(Number const & v)
      ^~~~~~

Con:

  • the default case is actually already a specialization (in this case double)
  • boilerplate constructors
  • we need to explicitly do this for all different number types and specialize for all integral types individually
  • the polymorphism is runtime polymorphism so its slower
  • we have to wrap the original types in our objects to achieve the behaviour

via templates and basic CRTP

Code (click to expand)

#include <iostream>
#include <type_traits>
#include <memory>

template <typename T>
struct Number
{
    T val;
    T doubleTheValue() const
    {
        std::cerr << "general funcion!\n";
        return val * 2;
    }
};

struct DoubleNumber : public Number<double>
{
};

struct IntNumber : public Number<int>
{
    int doubleTheValue() const
    {
        std::cerr << "special funcion!\n";
        return (val << 1);
    }
};

template <typename T>
void print2(T const & v)
{
    std::cout << v.doubleTheValue() << '\n';
}

int main()
{
//    print2("foo");
    print2(DoubleNumber{0.2});
    print2(IntNumber{2});

    return 0;
}

Pro:

  • the base class no longer contains a certain special type
  • no boilerplate constructors because we have aggregate types
  • static polymorphism, no runtime overhead

Con:

  • if we uncomment print2("foo"); we get an error inside print2, not it's signature:
doubleTheValue_crtp.cpp: In instantiation of 'void print2(const T&) [with T = char [4]]':
doubleTheValue_crtp.cpp:42:17:   required from here
doubleTheValue_crtp.cpp:37:20: error: request for member 'doubleTheValue' in 'v', which is of non-class type 'const char [4]'
     std::cout << v.doubleTheValue() << '\n';
                  ~~^~~~~~~~~~~~~~
  • in real-world code this is propagated very deep into the call-graph and makes it very hard to debug (note that we cannot use Number<T> in print2's interface, because IntNumber != Number<int>)
  • we need to explicitly do this for all different number types and specialize for all integral types individually
  • we still have to wrap the original types in our objects to achieve the behaviour

via template subclassing

Code (click to expand)

#include <iostream>
#include <type_traits>
#include <memory>

template <typename T>
struct Number
{
    T val;
};

template <typename T>
T doubleTheValue(Number<T> const & n)
{
    std::cerr << "general funcion!\n";
    return n.val * 2;
}

int doubleTheValue(Number<int> const & n)
{
    std::cerr << "special funcion!\n";
    return (n.val << 1);
};

template <typename T>
void print2(Number<T> const & v)
{
    std::cout << doubleTheValue(v) << '\n';
}

int main()
{
//    print2("foo");
    print2(Number<double>{0.2});
    print2(Number<int>{2});

    return 0;
}

Pro:

  • we don't need any inheritance anymore
  • we don't need to explicitly specialize for all types, only those that are specialized (but still for each of those seperately – unless we introduce another tag)

Pro/Con:

  • if we uncomment print2("foo"); we again get an error message related to it's signature:
doubleTheValue_templatesubclassing.cpp: In function 'int main()':
doubleTheValue_templatesubclassing.cpp:32:17: error: no matching function for call to 'print2(const char [4])'
     print2("foo");
                 ^
doubleTheValue_templatesubclassing.cpp:25:6: note: candidate: template<class T> void print2(const Number<T>&)
 void print2(Number<T> const & v)
      ^~~~~~
doubleTheValue_templatesubclassing.cpp:25:6: note:   template argument deduction/substitution failed:
doubleTheValue_templatesubclassing.cpp:32:17: note:   mismatched types 'const Number<T>' and 'const char [4]'
     print2("foo");
                 ^
  • although it's not very readable. In real world code, often this function we just a general T const & param, since it might handle objects that are not all of the same template; then we would get the same error message as above

Con:

  • we still have to wrap the original types in our template to achieve the specialization / it doesn't work for "third-party" types

via SFINAE

Code (click to expand)

#include <iostream>
#include <type_traits>

template <typename T>
std::enable_if_t<std::is_arithmetic<T>::value && !std::is_integral<T>::value, T> doubleTheValue(T const in)
{
    std::cerr << "general funcion!\n";
    return in * 2;
}

template <typename T>
std::enable_if_t<std::is_integral<T>::value, T> doubleTheValue(T const in)
{
    std::cerr << "special funcion!\n";
    return (in << 1);
}


template <typename T, std::enable_if_t<std::is_arithmetic<T>::value, int> = 0>
void print2(T const & v)
{
    std::cout << doubleTheValue(v) << '\n';
}

int main()
{
    print2("foo");
    print2(double{0.2});
    print2(int{2});
    print2(long{2});

    return 0;
}

Pro:

  • we don't need any kind of template or object wrapper, we work directly on the types
  • we don't need to explicitly specialize for any types anymore, because we rely on the metafunctions of the STL (it works for long, too)

Pro/Con:

  • if we uncomment print2("foo"); we again get an error message related to it's signature:
doubleTheValue_sfinae.cpp: In function 'int main()':
doubleTheValue_sfinae.cpp:27:17: error: no matching function for call to 'print2(const char [4])'
     print2("foo");
                 ^
doubleTheValue_sfinae.cpp:20:6: note: candidate: template<class T, typename std::enable_if<std::is_arithmetic<_Tp>::value, int>::type <anonymous> > void print2(const T&)
 void print2(T const & v)
      ^~~~~~
doubleTheValue_sfinae.cpp:20:6: note:   template argument deduction/substitution failed:
doubleTheValue_sfinae.cpp:19:77: error: no type named 'type' in 'struct std::enable_if<false, int>'
 template <typename T, std::enable_if_t<std::is_arithmetic<T>::value, int> = 0>
                                                                             ^
doubleTheValue_sfinae.cpp:19:77: note: invalid template non-type parameter
  • although it's not very readable.

Con:

  • the code is not very readable anymore :(
  • it doesn't work well for real specialization, because the general case needs to explicitly exclude the specialized cases

via concepts

Code (click to expand)

#include <iostream>
#include <type_traits>

template<typename T>
concept bool Number = std::is_arithmetic<T>::value;

template<typename T>
concept bool Integral = Number<T> && std::is_integral<T>::value;

Number doubleTheValue(Number const in)
{
    std::cerr << "general funcion!\n";
    return in * 2;
}

Integral doubleTheValue(Integral const in)
{
    std::cerr << "special funcion!\n";
    return (in << 1);
}

void print2(Number const & v)
{
    std::cout << doubleTheValue(v) << '\n';
}

int main()
{
//    print2("foo");
    print2(double{0.2});
    print2(int{2});
    print2(long{2});

    return 0;
}

Pro:

  • we don't need any kind of template or object wrapper, we work directly on the types
  • we don't need to explicitly specialize for any types anymore, because we rely on the metafunctions of the STL (it works for long, too)
  • specialization works as expected, the general concept or function does not need to anticipate the specialized one
  • we are very flexible with the concept definition, we could use a completely extrinsic one that requires certain operations, instead of the STL metafunction if we want to
  • if we uncomment print2("foo"); we get an error message related to it's signature:
doubleTheValue_concept.cpp: In function 'int main()':
doubleTheValue_concept.cpp:29:17: error: cannot call function 'void print2(const auto:3&) [with auto:3 = char [4]]'
     print2("foo");
                 ^
doubleTheValue_concept.cpp:22:6: note:   constraints not satisfied
 void print2(Number const & v)
      ^~~~~~
doubleTheValue_concept.cpp:5:14: note: within 'template<class T> concept const bool Number<T> [with T = char [4]]'
 concept bool Number = std::is_arithmetic<T>::value;
              ^~~~~~
doubleTheValue_concept.cpp:5:14: note: 'std::is_arithmetic<char[4]>::value' evaluated to false
  • the error message is not perfect, but it clearly illustrates that the first function in the call stack cannot be called, because it's not a number std::is_arithmetic<char[4]>::value' evaluated to false
  • the code is easy to read, even for people not familiar with concepts, the syntax is close to how it was originally done with inheritance
Clone this wiki locally