Reputation: 998
Given a template class A with a single template argument T, is it possible to only overload operators in A which are available for type T? For example:
template <typename T>
class A
{
public:
#if hasOperator(T, +=)
T& operator +=(const T &rhs)
{
mValue += rhs;
return mValue;
}
#endif
private:
T mValue;
}
int main()
{
A<int> a;
a += 8; //+= will forward to the += for the int
struct Test { /*no operators defined*/ };
A<Test> b; //+= is not implemented since Test does not implement +=
}
I'm writting a generic wrapper class that needs to behave exactly like the template type. So if T has operator +=, A will (at compile time) overload += accordingly. Yes, I could go ahead and just implement every operator in A, but then the compiler will error when T doesn't have a certain operator. At first I though template specialization might be the answer, but that would require a specialization for every type. While this could work and be a lot of typing, it wont because A needs to work with any type (not just what is specialized).
Upvotes: 5
Views: 887
Reputation: 275370
I will give 3 solutions in decreasing complexity and utility. The last solution is the simplest, and least complex.
A small, if useful, metaprogramming library:
template<class...>struct types{using type=types;};
namespace details {
template<template<class...>class Z, class types, class=void>
struct can_apply : std::false_type {};
template<template<class...>class Z, class...Ts>
struct can_apply<Z,types<Ts...>,std::void_t<Z<Ts...>>> :
std::true_type
{};
}
template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z,types<Ts...>>;
A trait for the result of +=
:
template<class Lhs, class Rhs>
using plus_equal_result = decltype(std::declval<Lhs>()+=std::declval<Rhs>());
template<class Lhs, class Rhs>
using can_plus_equal = can_apply< plus_equal_result, Lhs, Rhs >;
template<class T>
using can_self_plus_equal = can_plus_equal< T&, T const& >;
which gives us some nice traits that return true type or false type depending on if +=
is valid.
template<class A, class T, bool b = can_self_plus_equal<T>{}>
struct A_maybe_plus_equal {};
template<class A, class T>
struct A_maybe_plus_equal<A, T, true> {
A& self() { return *static_cast<A*>(this); }
A& operator+=( T && t )
{
self().mResult += std::move(t);
return self();
}
template<class U>
std::enable_if_t<can_plus_equal<T&,U>{},A&> operator+=( U && u )
{
self().mResult += std::forward<U>(u);
return self();
}
};
which gives us a +=
iff we pass true.
template <class T>
class A:
public A_maybe_plus_equal<A<T>, T>
{
friend class A_maybe_plus_equal<A<T>, T>;
public:
// nothing needed
private:
T mValue;
};
which gives you a +=
overload that takes a const T&
or a T&&
or a U&&
on the right hand side if and only if T& += T const&
is a valid expression.
This is the "perfect" solution, but it is complex.
Note that each operator can be done separately, so you don't have a combinatorial explosion of specializations.
Now, the there is an easier option. It has the downside that it doesn't support {}
based construction on the right hand side, and under some readings of the standard it is illegal.
It is, however, still SFINAE friendly:
template <typename T>
class A {
public:
template<class U>
auto operator +=(U&&rhs)
-> decltype( (std::declval<T&>()+=std::declval<U&&>()),void(),A& )
// or std::enable_if_t<can_plus_equal<T&,U>{},A&>
{
mValue += std::forward<U>(rhs);
return *this;
}
private:
T mValue;
};
This can be folded into the above option, and give both {}
and perfect forwarding syntax. I find that the T const&
can be dropped if you have a template
perfect forwarder.
The reason why this is technically undefined behavior is that the standard mandates that all template functions have at least one set of arguments that would render their body able to compile. In a given class instance, the template +=
of the above may have no such set of type arguments, which makes your program ill-formed with no diagnostic required (ie, UB).
There is another rule that member functions of template classes don't get instantiated unless called. Some argue that this other rule supersedes the one I mentioned in the last paragraph.
Another argument is that the method may be legal so long as there is some mixture of template arguments to the enclosing class(es) and to the template method itself that lead to it being instantiatable. I'd guess that this is what the standard committee intended, but I don't know how to read the standard to get this result.
This argument also applies to the plus_equal
function in answer #1. That implementation need not be as simple. In addition, #1 provides {}
based +=
syntax, which is a practical reason to use it. This concern -- that the program is technically ill-formed -- is academic, as all compilers I have used have no problems with this construct.
The paragraph three above this one gives us our final option. Do nothing.
template <typename T>
class A {
public:
A& operator +=(const T &rhs) {
mValue += rhs;
return *this;
}
private:
T mValue;
};
which means that you cannot SFINAE test that +=
doesn't work, but so long as you don't call +=
it "works". This is how vector
's operator<
works, for example. This is a lower "quality" solution, and cases of this in the standard library tend to be repaired over time.
However, as a first pass, this last choice is usually best. Only if you expect SFINAE requirements are the above hoops worthwhile.
Ultimately, C++1z is introducing concepts. I believe concepts will make this problem much easier, as eliminating overloads from consideration based on the type arguments of the enclosing class is a perennial problem in std
.
Upvotes: 4
Reputation: 302817
You don't actually have to do anything. Individual member functions of a template class won't get instantiatiated until use. You say:
but then the compiler will error when
T
doesn't have a certain operator.
But isn't that clearer than having it error when A<T>
doesn't? If you have:
template <typename T>
class A
{
public:
A& operator +=(const T &rhs)
{
mValue += rhs;
return *this;
}
A& operator-=(const T &rhs)
{
mValue -= rhs;
return *this;
}
// etc
private:
T mValue;
};
Then this will just work:
int main() {
A<int> a;
a += 8; //+= will forward to the += for the int
struct Test {
Test& operator-=(const Test& ) { return *this; }
};
A<Test> b;
b -= Test{}; // totally fine
b += Test{}; // error: no match for +=
// (operand types are 'main()::Test' and 'const main()::Test')
}
Upvotes: 1
Reputation: 109119
Use expression SFINAE to drop your operator+
from the overload resolution set unless T
defines operator+
template <typename T>
class A
{
private:
T mValue;
public:
template<typename U=T>
auto operator +=(const U &rhs)
-> decltype(mValue += rhs)
{
mValue += rhs;
return mValue;
}
};
Upvotes: 4