Search code examples
c++operatorsarithmetic-expressions

Why functional objects in C++ for arithmetic operations are implemented as templates?


I wonder, why functional objects in c++ are implemented as templated, with void as default type since c++14.

For example:

This object in fact performs arithmetic operation +, -, *, /, when called by operator().

The operator() has to be template to work with different types as arguments, but why does the struct have to be?

EDIT

I can create an operator std::plus<>, which may work with a different types in operator():

struct Foo{
    int foo;
};

Foo operator+(const Foo& lhs, const Foo& rhs){
    return {2 * lhs.foo + 3 * rhs.foo};
}

std::ostream& operator<<(std::ostream& os, const Foo& f){
    std::cout << f.foo;
    return os;
 }

int main()
{
    auto op = std::plus<>();
    std::cout << op(5, 3) << "\n";
    std::cout << op(3.14, 2.71) << "\n";
    std::cout << op(Foo(2), Foo(3)) << "\n";
}

And this gives the expected output. Or it may be the case, that having specified the type initially you get something more optimized?


Solution

  • It's a design choice. If you specify the type there is no template operator(), instead the whole class is a template. The operator() is simply something like

    constexpr T operator()(const T &lhs, const T &rhs) const 
    {
        return lhs + rhs;
    }
    

    This is in several ways different from having a template operator().

    If we pass a std::plus<int> it's a plus functor for specifically ints and nothing else.

    If we instead passed a std::plus<> without specifying the type it would have a templated operator(). That functor could apply it's operator() to any valid type.

    Some advantages with restricting the type from the top of my head:

    Since the type is specified the functor can deal with implicit conversion without any issues.

    You know for a fact that the functor is not going to silently do things we don't want it to. It's only ever going to do addition on Ts.

    Edit

    Some examples when the behaviour would differ.

    #include <iostream>
    #include <functional>
    #include <string>
    
    struct Foo {};
    
    
    int main()
    {
        auto stringadd = std::plus<std::string>{};
        auto anyadd = std::plus<>{};
    
        std::cout << stringadd("hey ", "you") << '\n';
        //std::cout << anyadd("hey ", "you") << '\n'; // error: no match for call to '(std::plus<void>) (const char [5], const char [4])'
    
        //std::cout << stringadd("hey ", 1) << '\n'; // error: no match for call to '(std::plus<std::__cxx11::basic_string<char> >) (const char [5], int)'
        std::cout << anyadd("hey ", 1) << '\n';
    }