12
\$\begingroup\$

I recently answered a question that had an interesting problem statement (I've slightly modified the wording):

I have an abstract class A which is base for the other classes, let's say B and C.

Classes B and C should have a handle_input() method, but argument number and types may vary from B to C.

I also have a function that takes a pointer to an A as an argument - polymorphically speaking, that could be either A, B or C - and calls the method handle_input().

I guess my question would be "How can you create a overriden method in base class when you don't know the arguments passed to the potentially multiple overriding functions?"

Basically, the user wants to polymorphically call functions whose signature might differ. In his specific scenario, the user has classes that all have a handle_input() function, but they differ in signature.

struct handler_a
{
    void handle_input( int );
};

struct handler_b
{
    void handle_input( std::string const&, char );
};

Normally, in order to handle this type of input, you have to create multiple handler functions, even though all we want is to execute the handle function with some pre-configured arguments.


My solution was very simplistic/bare-bones, so I decided to make a more robust implementation. Now that there's some meat attached, it's time for a review from you fine people before proceeding with other features.

Problems and solutions

Here I list questions I asked myself as well as my design solution. These show some thought process and hopefully someone might be able to suggest better design for specific parts of the code.

How can I get the fully qualified parameter types of a function?

By using template type deduction, a simple implementation is achieved. It must be used inside a decltype specifier. Overloads for different type of member functions are provided, these should cover all possible callables.

#include <tuple>
#include <type_traits>

template<class... Types>
struct type_list {};

template<class R>
auto make_forwarding_reference_tuple( R ) noexcept
-> decltype( make_forwarding_reference_tuple( &std::remove_pointer_t<R>::operator() ) );

template<class R, class... Args>
auto make_forwarding_reference_tuple( R( * )( Args... ) ) noexcept
-> type_list<Args...>;

template<class C, class R, class... Args>
auto make_forwarding_reference_tuple( R( C::* )( Args... ) ) noexcept
-> type_list<Args...>;

template<class C, class R, class... Args>
auto make_forwarding_reference_tuple( R( C::* )( Args... ) const ) noexcept
-> type_list<Args...>;

template<class C, class R, class... Args>
auto make_forwarding_reference_tuple( R( C::* )( Args... ) volatile ) noexcept
-> type_list<Args...>;

template<class C, class R, class... Args>
auto make_forwarding_reference_tuple( R( C::* )( Args... ) const volatile ) noexcept
-> type_list<Args...>;

Sample usage:

void f( int&& ) noexcept {}

int main()
{
    using type_list_t = decltype( make_forwarding_reference_tuple( &f ) );
    static_assert( std::is_same<type_list_t, type_list<int&&>>::value, "!" );
}

How can I call a callable with the values from a tuple as arguments, while perfect forwarding them at invocation?

This is accomplished by applying specified casts to the values of the parameter tuple as they as accessed through std::get<>() combined with a std::index_sequence<> during pack expansion. The specified casts are automatically generated as in the previous example.

template<class F, class Tuple, std::size_t... is, class... ForwardTypes>
static auto invoke_with_tuple( F&& f,
    Tuple&& t,
    std::index_sequence<is...>,
    type_list<ForwardTypes...> )
noexcept( noexcept( std::forward<F>( f )(
    static_cast<ForwardTypes>( std::get<is>( std::forward<Tuple>( t ) ) )... ) ) )
{
    return std::forward<F>( f )( static_cast<ForwardTypes>(
        std::get<is>( std::forward<Tuple>( t ) ) )... );
}

Sample usage:

#include <string>

void f( std::string&& s ) noexcept { std::string{ std::move( s ) }; }

int main()
{
    using fwd_list_t = type_list<std::string&&>;
    constexpr fwd_list_t fwd_list{};
    constexpr std::index_sequence<0> idx;
    invoke_with_tuple( f, std::make_tuple( std::string{ "a" } ), idx, fwd_list );
}

How can I store the arguments that will be used to invoke the callable?

First, we define the class hierarchy that provides polymorphic callable behaviour. The type will store the necessary information to perform a function call when the user wants it. Introducing the aptly named polymorphic_callable_base and polymorphic_callable<>:

class polymorphic_callable_base
{
public:
    polymorphic_callable_base() = default;
    polymorphic_callable_base( polymorphic_callable_base&& ) = default;
    polymorphic_callable_base( polymorphic_callable_base const& ) = default;
    polymorphic_callable_base& operator=( polymorphic_callable_base&& ) = default;
    polymorphic_callable_base& operator=( polymorphic_callable_base const& ) = default;
    virtual ~polymorphic_callable_base() = default;
    virtual void invoke() = 0;
};

template<class F, class... Args>
class polymorphic_callable : public polymorphic_callable_base
{
private:
    using tuple_t = std::tuple<Args...>;

    F f_;
    tuple_t args_;

    using fwd_type_list_t = decltype( make_forwarding_reference_tuple( f_ ) );
    static constexpr auto fwd_type_list = fwd_type_list_t{};
    static constexpr auto idx_seq = std::make_index_sequence<sizeof...( Args )>{};

public:
    ~polymorphic_callable() = default;

    template<class Fn, class... FnArgs>
    polymorphic_callable( Fn&& f, FnArgs&&... args )
    noexcept(
        std::is_nothrow_constructible<F, Fn&&>::value &&
        std::is_nothrow_constructible<tuple_t, FnArgs&&...>::value )
        : f_{ std::forward<Fn>( f ) }
        , args_{ std::forward<FnArgs>( args )... }
    {}

    polymorphic_callable( polymorphic_callable&& rhs ) = default;
    polymorphic_callable( polymorphic_callable const& rhs ) = default;
    polymorphic_callable& operator=( polymorphic_callable&& rhs ) = default;
    polymorphic_callable& operator=( polymorphic_callable const& rhs ) = default;

    void invoke()
    noexcept( noexcept(
        invoke_with_tuple( f_, args_, idx_seq, fwd_type_list ) ) ) override
    {
        invoke_with_tuple( f_, args_, idx_seq, fwd_type_list );
    }
};

template<class F, class... Args>
auto make_polymorphic_callable( F&& f, Args&&... args )
noexcept(
    std::is_nothrow_constructible<F, F&&>::value &&
    std::is_nothrow_constructible<std::tuple<std::decay_t<Args>...>, Args&&...>::value )
{
    return polymorphic_callable<F, std::decay_t<Args>...>
    {
        std::forward<F>( f ), std::forward<Args>( args )...
    };
}

For the deduced variadic template Args&&..., I apply std::decay_t<> so that polymorphic_callable<> gets the unqualified types and so that the instance of the tuple, and by extension polymorphic_callable<>, owns the arguments.

Sample usage:

#include <iostream>
#include <string>
#include <vector>

struct callable
{
    void operator()( int&&, char const& )
    {
        std::cout << "callable::operator()( int&&, char const& );\n";
    }
};

void f( std::string&& s ) noexcept { std::string{ std::move( s ) }; }

int main()
{
    std::string s{ "abc" };
    auto lda = [ s ] ( char&& ) { std::cout << "lambda[ std::string ]( char&& )\n"; };
    auto pc0 = make_polymorphic_callable( lda, 'a' );
    auto pc1 = make_polymorphic_callable( std::move( lda ), 'b' );
    auto pc2 = make_polymorphic_callable( callable{}, 5, 'c' );
    auto pc3 = make_polymorphic_callable( f, std::string{ "abc" } );

    std::vector<polymorphic_callable_base*> v{ &pc0, &pc1, &pc2, &pc3 };

    for ( auto pcb : v )
        pcb->invoke();
}

Extended

How can I obtain the result of an invocation?

Since the return type that can be represented by the same polymorphic_callable<> can differ and the signature of the overriden function invoke() function having a void return type, I introduce two new types: reader<T> and its friend writer<T>.

reader.h

The reader<T> type internally owns a value of type T. However, it can only read that value. Every reader<T> instance is created by some writer<T> instance. To ensure that references are kept valid, the constructing and assignment operations update the associated writer<T>.

#include <utility>
#include <type_traits>

template<class> class writer;

template<class T>
class reader
{
public:
    using value_type = T;

    /**
    * @brief sets the associated writer's pointer to nullptr
    */
    ~reader() noexcept( std::is_nothrow_destructible<value_type>::value )
    {
        writer_->value_ = nullptr;
    }

    /**
    * @brief takes ownership of the contained value of the argument reader as
    * if by move construction; associates the writer associated with the argument
    * reader with this reader.
    */
    reader( reader&& rhs )
    noexcept( std::is_nothrow_move_constructible<value_type>::value )
        : value_{ std::move( rhs.value_ ) }
        , writer_{ rhs.writer_ }
    {
        rhs.writer_->value_ = &value_;
    }

    /**
    * @brief takes ownership of the contained value of the argument reader as
    * if by move assignment; associates the writer associated with the argument
    * reader with this reader.
    */
    reader& operator=( reader&& rhs )
    noexcept( std::is_nothrow_move_assignable<value_type>::value )
    {
        writer_ = rhs.writer_;
        value_ = std::move( rhs.value_ );
        rhs.writer_->value_ = &value_;
    }

    reader( reader const& ) = delete;
    reader& operator=( reader const& ) = delete;

    /**
    * @brief access the contained value
    */
    value_type const& value() const noexcept
    {
        return value_;
    }

private:
    friend class writer<value_type>;

    /**
     * @brief constructs a reader and associates it with the parameter writer
     */
    reader( writer<value_type>* w )
        noexcept( std::is_nothrow_default_constructible<value_type>::value )
        : value_{}
        , writer_{ w }
    {
        w->value_ = &value_;
    }

    value_type value_;
    writer<value_type>* writer_;
};

template<>
class reader<void>
{
public:
    using value_type = void;
};

writer.h

The writer<T> type will write to an associated reader<T> instance that has been created by the last call to create_reader(). A call to set_value() sets the associated reader<T>'s value as if by move construction. If the associated reader<T> goes out of scope, calls to set_value() will be no ops, as I assume the result is no longer necessary when a reader<T> goes out of scope.

#include "reader.h"
#include <utility>
#include <type_traits>

template<class T>
class writer
{
public:
    using value_type = T;

    ~writer() = default;

    /**
    * @brief constructs a writer with no assicated reader.
    */
    constexpr writer() noexcept
        : value_{ nullptr }
    {}

    writer( writer&& rhs ) = default;
    writer( writer const& ) = default;
    writer& operator=( writer&& ) = default;
    writer& operator=( writer const& ) = default;

    /**
    * @brief sets the value associated with this writer; if there is no associated
    * value, the function does nothing.
    */
    template<class... Args>
    void set_value( Args&&... args )
    noexcept( std::is_nothrow_constructible<value_type, Args&&...>::value )
    {
        if ( value_ )
        {
            *value_ = value_type{ std::forward<Args>( args )... };
        }
    }

    /**
    * @brief creates a new reader and associates it to the reader.
    */
    reader<value_type> create_reader() noexcept
    {
        return reader<value_type>{ this };
    }

    /**
    * @brief indicates whether the reader has an associated writer or not.
    * @return true if the reader has an associated writer, false otherwise.
    */
    bool has_reader() const noexcept
    {
        return static_cast<bool>( value_ );
    }

private:
    friend class reader<value_type>;

    value_type* value_;
};

template<>
class writer<void>
{
public:
    using value_type = void;

private:
    friend class reader<value_type>;
};

Using these two types, we can redefine polymorphic_callable<>. It shall now have a create_reader() function that can be used by whoever wants to get the result of an invocation. Additionally, invoke()'s has tag dispatching based overloads to deal with void vs non-void return types.

template<class F, class... Args>
class polymorphic_callable : public polymorphic_callable_base
{
private:
    using tuple_t = std::tuple<Args...>;

    F f_;
    tuple_t args_;

    using fwd_type_list_t = decltype( make_forwarding_reference_tuple( f_ ) );
    static constexpr auto fwd_type_list = fwd_type_list_t{};
    static constexpr auto idx_seq = std::make_index_sequence<sizeof...( Args )>{};

    using result_t = decltype( invoke_with_tuple( f_, args_, idx_seq, fwd_type_list ) );

    writer<result_t> result_;

    void invoke( std::false_type )
    noexcept( noexcept(
        invoke_with_tuple( f_, args_, idx_seq, fwd_type_list ) ) )
    {
        result_.set_value( invoke_with_tuple( f_, args_, idx_seq, fwd_type_list ) );
    }

    void invoke( std::true_type )
    noexcept( noexcept(
        invoke_with_tuple( f_, args_, idx_seq, fwd_type_list ) ) )
    {
        invoke_with_tuple( f_, args_, idx_seq, fwd_type_list );
    }

public:
    ~polymorphic_callable() = default;

    template<class Fn, class... FnArgs>
    polymorphic_callable( Fn&& f, FnArgs&&... args )
    noexcept(
        std::is_nothrow_constructible<F, Fn&&>::value &&
        std::is_nothrow_constructible<tuple_t, FnArgs&&...>::value )
        : f_{ std::forward<Fn>( f ) }
        , args_{ std::forward<FnArgs>( args )... }
    {}

    polymorphic_callable( polymorphic_callable&& rhs ) = default;
    polymorphic_callable( polymorphic_callable const& rhs ) = default;
    polymorphic_callable& operator=( polymorphic_callable&& rhs ) = default;
    polymorphic_callable& operator=( polymorphic_callable const& rhs ) = default;

    void invoke()
    noexcept( noexcept(
        invoke_with_tuple( f_, args_, idx_seq, fwd_type_list ) ) ) override
    {
        using is_void_t = std::conditional_t
        <
            std::is_same<result_t, void>::value, std::true_type, std::false_type
        >;
        static constexpr is_void_t is_void{};

        invoke( is_void );
    }

    reader<result_t> create_reader()
    noexcept( std::is_nothrow_copy_constructible<result_t>::value )
    {
        return result_.create_reader();
    }
};

Sample usage:

#include <iostream>

int square_int( int i ) noexcept
{
    return i * i;
}

int main()
{
    auto pc = make_polymorphic_callable( square_int, 3 );
    auto pc_reader = pc.create_reader();
    pc.invoke();
    std::cout << pc_reader.value() << '\n';
}

Additional sample usage:

#include <iostream>
#include <string>
#include <vector>

struct callable
{
    int operator()( int&&, char const& )
    {
        std::cout << "callable::operator()( int&&, char const& );\n";
        return 42;
    }
};

int main()
{
    auto lda = [] () { std::cout << "lambda[ std::string ]( char&& )\n"; };
    auto pc0 = make_polymorphic_callable( lda );
    auto pc1 = make_polymorphic_callable( callable{}, 5, 'c' );

    // I'm particularly interested in pc1's return, so I ask for a reader.
    auto pc1_reader = pc1.create_reader();

    std::vector<polymorphic_callable_base*> v{ &pc0, &pc1 };

    // pc1 gets invoked at some point
    for ( auto pcb : v )
        pcb->invoke();

    // we know that we have pc1's result
    std::cout << pc1_reader.value() << '\n';
}

Considerations taken into account

  • Perfect forward at all times for efficiency.
  • The noexcept specifications should reflect the noexcept specification of the callable and any arguments used during construction and invocation.
  • During invocation, values shall be forwarded using the function's expected reference qualifier. Any behaviour from invoking the polymorphic_callable<> again is inherited.
  • Easy way to obtain results from any polymorphic_callable<> through the reader<T> and writer<T> types.
\$\endgroup\$
6
  • \$\begingroup\$ @OlzhasZhumabek I've added some explanation. If it is still unclear, please let me know and I will add further examples. \$\endgroup\$ Commented Sep 4, 2016 at 21:50
  • \$\begingroup\$ It is much clearer now. It's like one polymorphic function name, but other parts of the signature vary. \$\endgroup\$ Commented Sep 4, 2016 at 21:51
  • 1
    \$\begingroup\$ Have you looked at std::apply and std::invoke? \$\endgroup\$
    – Snowhawk
    Commented Sep 5, 2016 at 19:10
  • \$\begingroup\$ @Snowhawk04 I hadn't, they're not in my library. I will look into them. \$\endgroup\$ Commented Sep 5, 2016 at 19:19
  • 1
    \$\begingroup\$ What does prevent you to perfect forward the make_polymorphic_callable parameters into a lamda initialization list and create an std::function<void()> from it? It's almost no code and should work correctly with modern compilers (including MSVC). I'm asking because the std::function uses polymorphism to achieve its behavior. \$\endgroup\$
    – Dmitry T.
    Commented Jun 5, 2020 at 15:13

1 Answer 1

1
\$\begingroup\$

The problem is flawed to begin with

Basically, the user wants to polymorphically call functions whose signature might differ. In his specific scenario, the user has classes that all have a handle_input() function, but they differ in signature.

That is not going to work. If you need polymorphism, it means you only have a pointer to base. How do you know if this actually points to a derived class which has a handle_input() that accepts the right number of arguments? If you knew, then either you could have used an (intermediate) base class with the right number of arguments, or you know what the derived class is and can static_cast<>() the base pointer to a derived pointer.

If every derived class has the same set of overloads for handle_input(), then you just add all those overloads to the base class as well. The only interesting and valid problem I see is when you want variadic functions in the derived classes:

struct Base {
     template<typename... Args>
     virtual void handle_input(const Args&... args);
};

struct Derived: Base {
     template<typename... Args>
     void handle_input(const Args&... args) override {
         …
     }
};

The reason why you can't have templated virtual methods is that the compiler doesn't know up front what template instantiations are necessary, so it cannot build a vtable for it.

You didn't solve the originally stated problem

I am not saying that your solution isn't useful, and maybe it is indeed a good solution for whatever the original person tried to achieve, but the problem statement was: can I have a base class with a virtual member function that in the derived classes take different numbers of arguments? What you did instead was create a base class and derived classes that all have an invoke() function that takes no arguments at all.

Your solution isn't fully polymorphic

Let's look at these lines:

auto pc1 = make_polymorphic_callable( callable{}, 5, 'c' );
auto pc1_reader = pc1.create_reader();
std::cout << pc1_reader.value() << '\n';

If pc1 was a base class, then pc1.create_reader() could never work. After all, the base class doesn't know what function is going to be called, so it couldn't determine the return type of it. The only reason this works is because pc1 is a derived class. So while you can polymorphically call invoke(), you can't polymorphically get the results out.

Consider using std::function

I think that your code can simply be replaced with std::function and some lambdas. Consider this rewrite of your last example:

#include <iostream>
#include <vector>
#include <functional>

struct callable {
    int operator()( int&&, char const& ) {
        std::cout << "callable::operator()( int&&, char const& );\n";
        return 42;
    }
};

int main() {
    auto lda = [] { std::cout << "lambda[]()\n"; };
    auto pc0 = [&] { lda(); };
    int pc1_reader;
    auto pc1 = [&] { pc1_reader = callable{}( 5, 'c' ); };

    std::vector<std::function<void()>> v{ pc0, pc1 };

    for ( auto& pcb : v )
        pcb();

    std::cout << pc1_reader << '\n';
}

The "reader" is very simplistic here, and you need to be careful that the reader doesn't go out of scope before the function is called. The standard library comes with something like your reader and writer class, but even safer: std::future and std::promise.

Solving the original problem

I think the solution to the original problem would be to type-erase the parameters passed to the handle_input() function. This can be done in several ways, but a simple one, since C++17, is to pass and return a std::any:

#include <iostream>
#include <tuple>
#include <any>

struct Base {
    virtual std::any handle_input(std::any args) = 0;
};

struct Derived: Base {
    using args_type = std::tuple<int, char>;

    std::any handle_input(std::any args) override {
        auto [i, c] = std::any_cast<args_type>(args);
        std::cout << "handle_input(" << i << ", " << c << ");\n";
        return 42;
    }
};

int main() {
    Derived d;
    Base* b = &d;
    auto result = b->handle_input(std::make_tuple(5, 'c'));
    std::cout << std::any_cast<int>(result) << '\n';
}

Of course, the problem I mentioned at the beginning of my answer remains: does this make sense? If b is pointing to a derived class that std::any_cast<>()s to a different type, it will throw an exception.

There are other ways to type-erase the arguments. You could pass a std::function as an argument that somehow conveys the parameters to the function (as RyanP suggested in the comments to your answer). Yet another possibility, albeit a very complex one, is to make the equivalent of C++20's std::format_args.

\$\endgroup\$

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.