Very automatic operator generator in C++

2019-03-16 16:49发布

问题:

C++ has a nice idiom that allows one to write classes that automatically fulfill certain relations between operators. For example this allows to define operator== and not bother to defined operator!= as well. This is the idea behind Boost.Operators.

This is an example:

template<class Self> // this class is defined only once
struct equally_comparable{
    friend bool operator!=(Self const& s1, Self const& s2){return !(s1==s2);}
};

This class can used repeatedly to enforce consistent logic between == and != (and avoid errors)

struct A : equally_comparable<A>{ // 
    int value;
    A(int v) : value(v){}
    friend bool operator==(A const& a1, A const& a2){return a1.value == a2.value;}
};

int main(){

    A a1{4};
    A a2{4};
    assert(a1 == a2);
    A a3{5};
    assert(a1 != a3); // uses automatically generated operator !=
}

Now, I want to go one level further and have a class similar to equally_comparable and define the other function. For example if operator== is defined then define operator!= (like above), but also viceversa.

The first naive attempt works

template<class Self>
struct equally_comparable{
    friend bool operator!=(Self const& s1, Self const& s2){return !(s1==s2);}
    friend bool operator==(Self const& s1, Self const& s2){return !(s1!=s2);}
};

because only one of the two functions need to be defined in struct A (either operator== or operator!=). However it is dangerous because if one forget to define either operator in A there is an infinite recursion (and a runtime segfault). It also looks fragile.

Is is possible to improve over this and detect that at least one is defined in the derived class at compile time? or more generally is there a generic way to have a class that generates the missing operators? (i.e. a step beyond Boost.Operators).

回答1:

I would use SFINAE in order to check for comparators at compile time and choose them accordingly.

so this would work as follows:

  • create a tagging class (empty) here in the example it is called equal_comparable_tag
  • classes you want to exploit the additional overloads should be derived from this tag (since it is inherited the tag doesn't incur a size penalty).
  • the additional comparator functions have an enable_if guard because of function matching the non templated version is preferred. The guard checks if the class is derived from the tagging class.

working example

#include <type_traits>
#include <iostream>

struct equal_comparable_tag {};

struct tmp : public equal_comparable_tag {
    friend bool operator==(const tmp &, const tmp &) {
        std::cout << "baz";
        return false;
    }
};

struct tmp2 : public equal_comparable_tag {
    friend bool operator!=(const tmp2 &, const tmp2 &) {
        std::cout << "baz";
        return false;
    }
};

template<typename T>
typename std::enable_if<
std::is_base_of<equal_comparable_tag, T>::value,
bool
>::type
operator!=(const T &, const T&) {
    std::cout << "foo";
    return true;
}

template<typename T>
typename std::enable_if<
std::is_base_of<equal_comparable_tag, T>::value,
bool
>::type
operator==(const T &, const T&) {
    std::cout << "bar";
    return true;
}

int main(int argc, char** argv) {
    tmp a,b;
    tmp2 c,d;

    if (a != b) {} // foo
    if (a == b) {} // baz
    if (c != d) {} // baz
    if (c == d) {} // bar
}

Notes

Be aware, that automatic function signature checking can also be done (usually through type traits and function pointers See this article), but is considerable more complicated. This is required if you want to check if two functions are implemented. Be also aware that you need to check all possible combinations.

i.e.: if you have operator > and operator == you'd have to check:

  • has member function > and member function ==
  • has free function > and has free function ==
  • has free function > and member function ==
  • hash member function > and free function ==

and these checks fail if somebody uses implicit conversions e.g.:

struct tmp4 {
    tmp4(int) {
    }
    tmp4() {}
    operator int() const {
        return 10;
    }
    bool operator!=(int) const {
        std::cout << "baz";
        return false;
    }
};  
int main(int argc, char** argv) {
    tmp4 g, h;
    if (g != h) {} // compiles!
}