4
\$\begingroup\$

This utility function invokes one of a list of functions which are covariant on the provided value. It reduces boilerplate by internally performing dynamic_cast on the input value, and invokes the first function with an input type that was successfully cast. It must be invoked with a template argument indicating the return type. What do you think?

covariant_invoke.hpp

#ifndef UTILITIES_COVARIANT_INVOKE_HPP
#define UTILITIES_COVARIANT_INVOKE_HPP

namespace details {
    template <typename T>
    struct invoke_type_impl {
        static_assert(!std::is_same_v<T, T>, "template instantiation failed");
    };

    template <typename T, typename U, typename V>
    struct invoke_type_impl<T (U::*)(V const &) const> {
        typedef V in_t;
        typedef T out_t;
    };

    template <typename T>
    struct invoke_type {
        typedef typename invoke_type_impl<decltype(&T::operator ())>::in_t in_t;
        typedef typename invoke_type_impl<decltype(&T::operator ())>::out_t out_t;
    };
}

template<typename U, typename T, typename V1, typename std::enable_if<!std::is_same<U, void>::value, int>::type = 0>
U covariant_invoke(T const & value, V1 const & func1) {
    typedef typename details::invoke_type<V1>::in_t in_t;
    auto const * asU1 = dynamic_cast<in_t const *>(&value);
    if (asU1 != nullptr) {
        return func1(*asU1);
    }
    throw std::runtime_error("no suitable function");
}

template<typename U, typename T, typename V1, typename V2, typename... Vs, typename std::enable_if<!std::is_same<U, void>::value, int>::type = 0>
U covariant_invoke(T const & value, V1 const & func1, V2 const & func2, Vs const &... funcs) {
    typedef typename details::invoke_type<V1>::in_t in_t;
    auto const * asU1 = dynamic_cast<in_t const *>(&value);
    if (asU1 != nullptr) {
        return func1(*asU1);
    }
    return covariant_invoke<U>(value, func2, funcs...);
}

template<typename U, typename T, typename V1, typename std::enable_if<std::is_same<U, void>::value, int>::type = 0>
void covariant_invoke(T const & value, V1 const & func1) {
    typedef typename details::invoke_type<V1>::in_t in_t;
    static_assert(std::is_same<typename details::invoke_type<V1>::out_t, void>::value, "function does not return void");
    auto const * asU1 = dynamic_cast<in_t const *>(&value);
    if (asU1 != nullptr) {
        func1(*asU1);
        return;
    }
    throw std::runtime_error("no suitable function");
}

template<typename U, typename T, typename V1, typename V2, typename... Vs, typename std::enable_if<std::is_same<U, void>::value, int>::type = 0>
void covariant_invoke(T const & value, V1 const & func1, V2 const & func2, Vs const &... funcs) {
    typedef typename details::invoke_type<V1>::in_t in_t;
    static_assert(std::is_same<typename details::invoke_type<V1>::out_t, void>::value, "function does not return void");
    auto const * asU1 = dynamic_cast<in_t const *>(&value);
    if (asU1 != nullptr) {
        func1(*asU1);
        return;
    }
    covariant_invoke<U>(value, func2, funcs...);
}

#endif //UTILITIES_COVARIANT_INVOKE_HPP

Example Usage

#include <iostream>

class A {
public:
    virtual ~A() = default;
};

class B : public A {
public:
    explicit B(int value) : i(value) {}
    virtual ~B() = default;
    int i;
};

class C : public A {
public:
    explicit C(char* value) : strPtr(value) {}
    virtual ~C() = default;
    char* strPtr;
};

class D : public A {
public:
    virtual ~D() = default;
};

B b = B(1337);
C c = C("Hello, world!");
D d;

// test the non-void return overload
void test_non_void_return(A const & a) {
    std::cout << covariant_invoke<std::string>(a,
        [&](B const & value) { return std::to_string(value.i); },
        [&](C const & value) { return value.strPtr; });
}

// test the void return overload
void test_void_return(A const & a) {
    covariant_invoke<void>(a,
        [&](B const & value) { std::to_string(value.i); },
        [&](C const & value) { value.strPtr; });
}

void test() {
    test_non_void_return(b);
    test_non_void_return(c);
    test_non_void_return(d); //will throw, because there is no lambda to handle D values

    test_void_return(b);
    test_void_return(c);
    test_void_return(d); //will throw, because there is no lambda to handle D values
}
\$\endgroup\$
4
  • \$\begingroup\$ What's the question? \$\endgroup\$
    – KjMag
    Commented Jul 2, 2017 at 18:29
  • 2
    \$\begingroup\$ @KjMag I thought it was implied. I've added the question. \$\endgroup\$
    – Brent
    Commented Jul 2, 2017 at 18:43
  • 1
    \$\begingroup\$ What problem does this code solve? I know what it does, but why do you need it? To me it looks like a terribly inefficient virtual function that's hard to read. \$\endgroup\$
    – Emily L.
    Commented Jul 2, 2017 at 23:42
  • \$\begingroup\$ I have a graph algorithm with several node types distinguished by class and all inheriting a base node type. This is used to help in implementation of a visitor pattern. \$\endgroup\$
    – Brent
    Commented Jul 3, 2017 at 12:41

1 Answer 1

1
\$\begingroup\$

I believe there is a better way to solve the problem.

Alt. solution #1

First thing would be using dynamic polymorphism and semi double dispatch by using visitor pattern. This is generally well understood by anyone who knows what polymorphism is. The technique is very old.

Alt. solution #2

std::variant<> and std::visit(). I believe boost have backports to older C++ standards, since the standard ones require C++17 conformant standard library, may be even compiler.


Both of the solutions are better understood by people than the presented one. The fact suggests than engineering costs of the alternative solutions are much lower, which makes them more viable in the real world.

Code Review

Code formatting

I believe some vertical spacing inside of the functions could make them more readable. Also splitting too long lines into two would be great. Not all IDEs are fine with splitting template parameter declaration into multiple lines, but making it by hand shouldn't be too hard.

\$\endgroup\$
3
  • \$\begingroup\$ The generic C++ ABI used on Linux, BSDs and many more simply does a pointer-comparison for determining whether two type_info-objects are equal... \$\endgroup\$ Commented Jul 7, 2017 at 15:38
  • \$\begingroup\$ @Deduplicator, great stuff. I'll rewrite that portion when I'll have time and gathering more info. \$\endgroup\$ Commented Jul 7, 2017 at 15:40
  • \$\begingroup\$ @Deduplicator using type_info tends to be cheap, using dynamic_cast has the reputation of being slow. See this answer. \$\endgroup\$
    – Maikel
    Commented Aug 7, 2017 at 7:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.