This is just another case for void_t
.
We need a little helper template Void
and define a convenience template type alias void_t
.
#include <type_traits>
template<typename...>
struct Void { using type = void; };
template<typename... T>
using void_t = typename Void<T...>::type;
We define the primary template that implements the fallback policy.
template<typename T, typename = void>
struct Helper
{
static void
function(T& t)
{
std::cout << "doing something else with " << &t << std::endl;
}
};
And provide a partial specialization for types that support a specific operation, in this case, .data().push_back(int)
.
template<typename T>
struct Helper<T, void_t<decltype(std::declval<T>().data().push_back(0))>>
{
static void
function(T& t)
{
std::cout << "pushing back data to " << &t << std::endl;
t.data().push_back(42);
}
};
To hide the Helper
implementation detail from our clients and to allow type deduction for the template parameters, we can nicely wrap it up.
template<typename T>
void
function(T& t)
{
Helper<T>::function(t);
}
And this is how our clients use it.
#include <iostream>
#include <vector>
class Alpha
{
public:
std::vector<int>& data() { return this->data_; }
private:
std::vector<int> data_ {};
};
class Beta { /* has no data() */ };
int
main()
{
Alpha alpha {};
Beta beta {};
std::cout << "&alpha = " << &alpha << std::endl;
std::cout << "&beta = " << &beta << std::endl;
function(alpha);
function(beta);
}
Possible output:
&alpha = 0x7ffffd2a3eb0
&beta = 0x7ffffd2a3eaf
pushing back data to 0x7ffffd2a3eb0
doing something else with 0x7ffffd2a3eaf
Update: How to apply this technique to multiple members
The technique shown above can be applied to any number of members. Let's make up a little example. Say we want to write a template function frobnicate
that takes an argument of generic type and if the object has…
- …a member function
incr
that takes no arguments, call it,
- …a data member
name
, append some text to it if possible and
- …a data member
numbers
, push_back
some numbers to it if possible.
I really recommend you solve this by implementing three helper struct
s as shown above. It is not that much redundant typing and makes for much cleaner code.
However, if you wish to ignore this advice, let's see how we can reduce the typing by using a macro. Assuming the same definition of void_t
as shown above, we can define the following macro.
#define MAKE_SFINAE_HELPER(NAME, TYPE, OPERATION, ARGS, CODE) \
template<typename TYPE, typename = void> \
struct NAME \
{ \
template<typename... AnyT> \
void \
operator()(AnyT&&...) noexcept \
{ \
/* do nothing */ \
} \
}; \
\
template<typename TYPE> \
struct NAME<TYPE, void_t<decltype(std::declval<TypeT>()OPERATION)>> \
{ \
void operator()ARGS noexcept(noexcept(CODE)) \
{ \
CODE; \
} \
};
It will define a struct
called NAME
templated on a type parameter TYPE
and define a primary template with an operator ()
that takes any number of arguments of any type and does absolutely nothing. This is used as the fallback if the desired operation is not supported.
However, if an object of type TYPE
supports the operation OPERATION
, then the partial specialization with an operator ()
that takes parameters ARGS
and executes CODE
will be used. The macro is defined such that ARGS
can be a parenthesized argument list. Unfortunately, the preprocessor grammar only allows for a single expression to be passed as CODE
. This is not a big problem as we can always write a single function call that delegates to another function. (Remember that any problem in computer science can be solved by adding an extra level of indirection – except, of course, for the problem of too many levels of indirection…) The operator ()
of the partial specialization will be declared noexcept
if and only if CODE
is. (This also only works because CODE
is restricted to a single expression.)
The reason that the operator ()
for the primary template is a template is that otherwise the compiler might emit warnings about unused variables. Of course, you can alter the macro to accept an additional parameter FALLBACK_CODE
that is placed in the body of the primary template's operator ()
that should use the same ARGS
then.
In the most simple cases, it might be possible to combine the OPERATION
and the CODE
parameter into one but then CODE
cannot refer to ARGS
which effectively limits ARGS
to a single parameter of type TYPE
in which case you could get rid of that parameter as well, if you don't need the flexibility.
So, let's apply this to our problem. First, we need a helper function for pushing back the numbers because this cannot be written (at least, let's pretend this) as a single expression. I make this function as generic as possible, making only assumptions on the member name.
template<typename ObjT, typename NumT>
void
do_with_numbers(ObjT& obj, NumT num1, NumT num2, NumT num3)
{
obj.numbers.push_back(num1);
obj.numbers.push_back(num2);
obj.numbers.push_back(num3);
}
Since the other two desired operations can easily be written as a single expression, we need no further indirection for them. So now, we can generate our SFINAE helpers.
MAKE_SFINAE_HELPER(HelperIncr,
TypeT,
.incr(),
(TypeT& obj),
obj.incr())
MAKE_SFINAE_HELPER(HelperName,
TypeT,
.name += "",
(TypeT& obj, const std::string& appendix),
obj.name += appendix)
MAKE_SFINAE_HELPER(HelperNumbers,
TypeT,
.numbers.push_back(0),
(TypeT& obj, int i1, int i2, int i3),
do_with_numbers(obj, i1, i2, i3))
Equipped with these, we can finally write our frobnicate
function. It's really simple.
template<typename T>
void
frobnicate(T& object)
{
HelperIncr<T>()(object);
HelperName<T>()(object, "def");
HelperNumbers<T>()(object, 4, 5, 6);
}
To see that everything works, let's make two struct
s that partially support the operations in question.
#include <string>
#include <vector>
struct Widget
{
std::vector<int> numbers {1, 2, 3};
int counter {};
void incr() noexcept { this->counter += 1; }
};
struct Gadget
{
std::string name {"abc"};
int counter {};
void incr() noexcept { this->counter += 1; }
};
Since I want to print them, let's also define operators <<
.
#include <iostream>
std::ostream&
operator<<(std::ostream& os, const Widget& w)
{
os << "Widget : { counter : " << w.counter << ", numbers : [";
int i {};
for (const auto& v : w.numbers)
os << (i++ ? ", " : "") << v;
os << "] }";
return os;
}
std::ostream&
operator<<(std::ostream& os, const Gadget& g)
{
os << "Gadget : { counter : " << g.counter << ", "
<< "name = \"" << g.name << "\" }";
return os;
}
And there we go:
int
main()
{
Widget widget {};
Gadget gadget {};
std::cout << widget << "\n" << gadget << "\n\n";
frobnicate(widget);
frobnicate(gadget);
std::cout << widget << "\n" << gadget << "\n";
}
Output:
Widget : { counter : 0, numbers : [1, 2, 3] }
Gadget : { counter : 0, name = "abc" }
Widget : { counter : 1, numbers : [1, 2, 3, 4, 5, 6] }
Gadget : { counter : 1, name = "abcdef" }
I encourage you to carefully gauge the costs and benefits of this macro approach. In my opinion, the extra complexity is barely worth the small savings on the typing.