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 sayB
andC
.Classes
B
andC
should have ahandle_input()
method, but argument number and types may vary fromB
toC
.I also have a function that takes a pointer to an
A
as an argument - polymorphically speaking, that could be eitherA
,B
orC
- and calls the methodhandle_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 thenoexcept
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 thereader<T>
andwriter<T>
types.
std::apply
andstd::invoke
? \$\endgroup\$make_polymorphic_callable
parameters into a lamda initialization list and create anstd::function<void()>
from it? It's almost no code and should work correctly with modern compilers (including MSVC). I'm asking because thestd::function
uses polymorphism to achieve its behavior. \$\endgroup\$