20

In case when static polymorphism is used, especially in templates (e.g. with policy/strategy pattern), it may be required to call base function member, but you don't know was instantiated class actually derived from this base or not.

This easily can be solved with old good C++ ellipsis overload trick:

#include <iostream>

template <class I>
struct if_derived_from
{
    template <void (I::*f)()>
    static void call(I& x) {  (x.*f)(); }

    static void call(...) { }
};

struct A    { void reset() { std::cout << "reset A" << std::endl; } };
struct B    { void reset() { std::cout << "reset B" << std::endl; } };
struct C    { void reset() { std::cout << "reset C" << std::endl; } };
struct E: C { void reset() { std::cout << "reset E" << std::endl; } };
struct D: E {};

struct X: A, D {};

int main()
{
    X x;
    if_derived_from<A>::call<&A::reset>(x);
    if_derived_from<B>::call<&B::reset>(x);
    if_derived_from<C>::call<&C::reset>(x);
    if_derived_from<E>::call<&E::reset>(x);
    return 0;
}

The question is:

  • Is there any better simple way (e.g. SFINAE doesn't look so) to achieve same result in C++11/C++14?
  • Would empty call of ellipsis parameter function be elided by optimizing compiler? Hope such case is not special against any "normal" function.
Rost
  • 8,779
  • 28
  • 50

3 Answers3

14

One option is to introduce two overloads of different priorities and to equip the preferred one with an expression SFINAE.

#include <utility>

template <typename T, typename... Args, typename C, typename R, typename... Params>
auto call_impl(int, R(C::*f)(Args...), T&& t, Params&&... params)
    -> decltype((std::forward<T>(t).*f)(std::forward<Params>(params)...))
{
    return (std::forward<T>(t).*f)(std::forward<Params>(params)...);
}

template <typename T, typename... Args, typename C, typename R, typename... Params>
void call_impl(char, R(C::*)(Args...), T&&, Params&&...)
{
}

template <typename T, typename... Args, typename C, typename R, typename... Params>
auto call(R(C::*f)(Args...), T&& t, Params&&... params)
    -> decltype(call_impl(0, f, std::forward<T>(t), std::forward<Params>(params)...))
{
    return call_impl(0, f, std::forward<T>(t), std::forward<Params>(params)...);
}

Test:

int main()
{
    X x;
    call(&B::reset, x);
}

DEMO

The upper function will be selected first by overload resolution (due to an exact match of 0 against int), and possibly excluded from the set of viable candidates if (t.*f)(params...) is not valid. In the latter case, the call to call_impl falls back to the second overload, which is a no-op.


Given that &A::reset may fail for multiple reasons, and you may not necessarily want to explicitly specify the function's signature, and, on top of that, you want the call to fail if the member function exists, but it does not match function call arguments, then you can exploit generic lambdas:

#include <utility>
#include <type_traits>

template <typename B, typename T, typename F
        , std::enable_if_t<std::is_base_of<B, std::decay_t<T>>{}, int> = 0>
auto call(T&& t, F&& f)
    -> decltype(std::forward<F>(f)(std::forward<T>(t)))
{
    return std::forward<F>(f)(std::forward<T>(t));
}

template <typename B, typename T, typename F
        , std::enable_if_t<!std::is_base_of<B, std::decay_t<T>>{}, int> = 0>
void call(T&& t, F&& f)
{
}

Test:

int main()
{
    X x;
    call<A>(x, [&](auto&& p) { return p.A::reset(); });
    call<B>(x, [&](auto&& p) { return p.B::reset(); });
}

DEMO 2

Piotr Skotnicki
  • 46,953
  • 7
  • 118
  • 160
  • Nice trick with 0 exact/implicit match, thanks! The only gotcha with this solution is that it wouldn't fail to compile in case when X is derived from A and A has reset() function member but with different signature, e.g. A::reset(int). In this case it would just silently omit the A::reset() call which can be undesirable. – Rost Mar 01 '16 at 09:20
  • @Rost then you'd call it like `call(&A::reset, x, 42);`, but you want a compile error for `call(&A::reset, x);` ? – Piotr Skotnicki Mar 01 '16 at 09:21
  • Piotr, yes, that is desirable. – Rost Mar 01 '16 at 09:26
  • Piotr, yes, this does the trick, but unfortunately simplicity escaped... Anyway, thanks! – Rost Mar 01 '16 at 10:10
  • @Rost then maybe [this](http://coliru.stacked-crooked.com/a/c9a100c0605adacb) ? this one will always work, for any overload (note that `&A::reset` won't work if it's overloaded) and any args mismatch – Piotr Skotnicki Mar 01 '16 at 10:17
13

what about something like:

#include <iostream>
#include <type_traits>

struct A    { void reset() { std::cout << "reset A" << std::endl; } };
struct B    { void reset() { std::cout << "reset B" << std::endl; } };

struct X :public A{};

template <typename T, typename R, typename BT>
typename std::enable_if<std::is_base_of<BT, T>::value, R>::type
call_if_possible(T & obj, R(BT::*mf)())
{
    return (obj.*mf)();
}

template <typename T, typename R, typename BT>
typename std::enable_if<!std::is_base_of<BT, T>::value, R>::type
call_if_possible(T & obj, R(BT::*mf)()) { }

int main()
{
    X x;

    call_if_possible(x, &A::reset);
    call_if_possible(x, &B::reset);
}

ideone

edit

maybe more readable way:

template <typename T, typename R, typename BT>
R call_if_possible_impl(T & obj, R(BT::*mf)(), std::false_type){}

template <typename T, typename R, typename BT>
R call_if_possible_impl(T & obj, R(BT::*mf)(), std::true_type)
{
    return (obj.*mf)();
}

template <typename T, typename R, typename BT>
R call_if_possible(T & obj, R(BT::*mf)())
{
    return call_if_possible_impl(obj, mf, typename std::is_base_of<BT, T>::type());
}

ideone

relaxxx
  • 7,566
  • 8
  • 37
  • 64
  • it could be more readable using std::void_t (I will try to write that) ... I'm not sure how to get rid the redundant `enable_if` in 2nd specialization – relaxxx Mar 01 '16 at 09:01
  • but I find it more readable than ellipsis version (I don't really like ellipsis) – relaxxx Mar 01 '16 at 09:01
  • unfortunately `std::void_t` is trick used when specialization comes into play and functions templates cannot be specialized... and when wrapping all into `struct`, kills deducing the member-function type deduction... sigh – relaxxx Mar 01 '16 at 09:10
  • It's funny how the readable code is unreadable to someone who learned C and C#... – Ismael Miguel Mar 01 '16 at 10:30
  • 2
    @IsmaelMiguel that's because it's not C or C# ;) – relaxxx Mar 01 '16 at 10:43
  • I know, I know... But isn't C++ supposed to be somewhat backwards compatible? – Ismael Miguel Mar 01 '16 at 11:07
  • 1
    @IsmaelMiguel it "is" http://stackoverflow.com/questions/4326045/is-new-c-backward-compatible – relaxxx Mar 01 '16 at 11:11
  • @relaxxx Looks better, but it's sticked to mf signature. I like the lambda approach by Piotr. – Rost Mar 01 '16 at 12:51
  • 1
    I used member function instead of a lambda to answer your question as closely as possible ;) – relaxxx Mar 01 '16 at 14:26
4

Basing on previously provided answers by @PiotrSkotnicki and @relaxxx I would like to combine the most simple and readable solution, without SFINAE and other blood-from-the-eyes things. It's just for reference, will not be accepted anyway:

#include <iostream>
#include <type_traits>

template <class Base, class Derived>
using check_base = typename std::is_base_of<Base, Derived>::type;

template <class Base, class Derived, typename Func>
void call(Derived& d, Func&& f)
{
    call<Base>(d, std::forward<Func>(f), check_base<Base, Derived>());
}

template <class Base, typename Func>
void call(Base& b, Func&& f, std::true_type)
{
    f(b);
}

template <class Base, class Derived, typename Func>
void call(Derived&, Func&&, std::false_type)
{
}

struct A    { void reset(int i) { std::cout << "reset A: " << i << std::endl;} };
struct B    { void reset()      { std::cout << "reset B" << std::endl;} };
struct C    { void reset()      { std::cout << "reset C" << std::endl;} };
struct E: C { void reset()      { std::cout << "reset E" << std::endl;} };

struct D: A, E {};

int main()
{
    D d;
    int i = 42;
    call<A>(d, [&](auto& p) { p.reset(i); } );
    call<B>(d, [](auto& p)  { p.reset(); } );
    call<C>(d, [](auto& p)  { p.reset(); } );
    call<E>(d, [](auto& p)  { p.reset(); } );
}

Live at: http://cpp.sh/5tqa

Rost
  • 8,779
  • 28
  • 50
  • 3
    Note that there's a difference between `static_cast(d).reset()` and `d.Base::reset()`, the former involves late binding, the latter does not – Piotr Skotnicki Mar 01 '16 at 13:26
  • @PiotrSkotnicki, yes, I understand, but it's not intended to be used with virtual functions. It's about static polymorphism only. – Rost Mar 01 '16 at 13:33