Boost C++ Libraries Home Libraries People FAQ More

Next

CallableTraits

Barrett Adair

Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.md or copy at http://www.boost.org/LICENSE_1_0.txt)


Table of Contents

Introduction
Prerequisite Topics
Motivation
Quick Example
Example: std::function sugar
Compatibility
MSVC Issues
FAQ
Why should I use CallableTraits?
What makes CallableTraits unique?
Why does callable_traits::args alias a std::tuple?
Why are the CallableTraits alias templates not suffixed with _t, as in std::add_lvalue_reference_t?
Why use constexpr function templates? Why not use something like std::is_same or even std::is_same_v?
Why use reference collapsing rules when adding member function ref-qualifiers?
Contact
Concepts
FunctionPtr
FunctionReference
UnqualifiedFunction
AbominableFunction
Function
MemberFunctionPtr
MemberDataPtr
MemberPtr
SimpleFunctionObject
OverloadedFunctionObject
FunctionObject
Signature
Callable
SimpleCallable
SimpleInvokable
Invokable
BindExpression
ConstexprDefaultConstructible
CallingConventionTag
Headers
Qualifiers
Parameters
Return Types
INVOKE
Member Pointers
Constant Expressions
Variadics
Calling Conventions (experimental)
add_calling_convention
add_member_const
add_member_cv
add_member_lvalue_reference
add_member_rvalue_reference
add_varargs
add_member_volatile
apply_member_pointer
apply_return
arg_at
args
arity
bind
can_invoke
can_invoke_constexpr
clear_args
expand_args
function_type
has_calling_convention
has_varargs
has_void_return
is_const_member
is_constexpr
is_lvalue_reference_member
is_reference_member
is_rvalue_reference_member
has_member_qualifiers
is_volatile_member
max_arity
min_arity
qualified_function_type
remove_calling_convention
remove_member_const
remove_member_cv
remove_member_pointer
remove_member_reference
remove_varargs
remove_member_volatile
push_args_back
push_args_front
result_of
Acknowledgements

CallableTraits is a cross-platform C++14 library for the inspection, decomposition, and synthesis of C++ callable types. CallableTraits is header-only, and does not depend on any non-standard headers. CallableTraits is currently hosted at GitHub.

[Note] Note

CallableTraits is not a Boost library.

Don't try to write helper code to detect PMFs/PMDs and dispatch on them -- it is an absolute nightmare. PMF types are the worst types by far in the core language.

-- Stephan T. Lavavej, CppCon 2015, "functional: What's New, And Proper Usage"

Consider for a moment the class template below, which defines all 48 template specializations necessary to account for all valid function types and member function pointer types in C++11 and C++14:

template<typename T> struct foo;

//function type without varargs
template<class Return, class... Args> struct foo<Return(Args...)> {};
template<class Return, class... Args> struct foo<Return(Args...) &> {};
template<class Return, class... Args> struct foo<Return(Args...) &&> {};
template<class Return, class... Args> struct foo<Return(Args...) const> {};
template<class Return, class... Args> struct foo<Return(Args...) const &> {};
template<class Return, class... Args> struct foo<Return(Args...) const &&> {};
template<class Return, class... Args> struct foo<Return(Args...) volatile> {};
template<class Return, class... Args> struct foo<Return(Args...) volatile &> {};
template<class Return, class... Args> struct foo<Return(Args...) volatile &&> {};
template<class Return, class... Args> struct foo<Return(Args...) const volatile> {};
template<class Return, class... Args> struct foo<Return(Args...) const volatile &> {};
template<class Return, class... Args> struct foo<Return(Args...) const volatile &&> {};

//function type with varargs
template<class Return, class... Args> struct foo<Return(Args..., ...)> {};
template<class Return, class... Args> struct foo<Return(Args..., ...) &> {};
template<class Return, class... Args> struct foo<Return(Args..., ...) &&> {};
template<class Return, class... Args> struct foo<Return(Args..., ...) const> {};
template<class Return, class... Args> struct foo<Return(Args..., ...) const &> {};
template<class Return, class... Args> struct foo<Return(Args..., ...) const &&> {};
template<class Return, class... Args> struct foo<Return(Args..., ...) volatile> {};
template<class Return, class... Args> struct foo<Return(Args..., ...) volatile &> {};
template<class Return, class... Args> struct foo<Return(Args..., ...) volatile &&> {};
template<class Return, class... Args> struct foo<Return(Args..., ...) const volatile> {};
template<class Return, class... Args> struct foo<Return(Args..., ...) const volatile &> {};
template<class Return, class... Args> struct foo<Return(Args..., ...) const volatile &&> {};

//member function pointer type without varargs
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args...)> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args...) &> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args...) &&> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args...) const> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args...) const &> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args...) const &&> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args...) volatile> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args...) volatile &> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args...) volatile &&> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args...) const volatile> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args...) const volatile &> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args...) const volatile &&> {};

//member function pointer type with varargs
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args..., ...)> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args..., ...) &> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args..., ...) &&> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args..., ...) const> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args..., ...) const &> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args..., ...) const &&> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args..., ...) volatile> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args..., ...) volatile &> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args..., ...) volatile &&> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args..., ...) const volatile> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args..., ...) const volatile &> {};
template<class Return, class T, class... Args> struct foo<Return(T::*)(Args..., ...) const volatile &&> {};
[Note] Note

The upcoming ISO standard for C++17 includes a change to the core language which adds the noexcept qualifier to the type system. If the author's understanding is correct, this would increase the the count of necessary template specializations to 96 (feedback?). Currently, C++17 noexcept qualifiers are not handled in CallableTraits, because compiler vendors have not implemented support for this feature (to best of the author's knowledge). However, features to account for and manipulate noexcept qualifiers are planned in CallableTraits, as soon as feature is implemented by a compiler vendor. Of course, the CallableTraits feature additions will be non-breaking for currently supported compiler versions.

Use cases for such obscure specializations are vitually nonexistent in run-of-the-mill application codebases. Even in library code, these are exceedingly rare. However, there are a handful of very specific metaprogramming scenarios that can only be solved with such template "spam". While these use cases are indeed rare, the writing and testing of these templates is incredibly tedious and time consuming. On this premise, CallableTraits offers a final and decisive library-level solution, so that authors of generic code will never again need to write these specializations, for any reason.

Template specializations like those in the code snippet above still do not account for function pointers, function references, function objects/lambdas, or calling conventions. CallableTraits goes the extra mile by accounting for all of them.

[Warning] Warning

Features for the manipulation and inspection of calling conventions are optional, which must be enabled with macro definitions. The features regarding them are currently classified as experimental, and are subject to breaking changes in future versions of CallableTraits.

The rationale for classifying the calling convention features as "experimental" is three-fold:

  1. Calling conventions are, by definition, highly platform-specific.
  2. The inclusion of a single calling convention effectively doubles the test surface of CallableTraits on every platform.
  3. The author's current knowledge of calling conventions is admittedly limited.

It is the author's hope that future versions of CallableTraits will offer a stable, thoroughly-tested, and well-documented implementation of these features. If you have experience in this domain, your feedback is highly appreciated.

The use cases for CallableTraits are closely related to those of function_traits and Boost.FunctionTypes.

This short program showcases some, but not all, of the features available in CallableTraits.

#include <type_traits>
#include <functional>
#include <tuple>
#include <callable_traits/callable_traits.hpp>

namespace ct = callable_traits;

// foo is an example of a function object
struct foo {
    void operator()(int, int&&, const int&, void* = nullptr) const {}
};

int main() {

    // indexed argument types
    using second_arg = ct::arg_at<1, foo>;
    static_assert(std::is_same<second_arg, int&&>::value, "");

    // arg types are packaged into std::tuple, which serves as the default
    // type list in CallableTraits (runtime capabilities are not used).
    using args = ct::args<foo>;
    using expected_args = std::tuple<int, int&&, const int&, void*>;
    static_assert(std::is_same<args, expected_args>::value, "");

    // callable_traits::function_type "decays" a callable type to a plain
    // function type, which is structured in terms of INVOKE.
    using function_type = ct::function_type<foo>;
    using expected_function_type = void(int, int&&, const int&, void*);
    static_assert(std::is_same<function_type, expected_function_type>::value, "");

    // By design, the CallableTraits interface uses constexpr
    // std::integral_constant functions (whenever sensible).
    // By also defining the appropriate overloads, this gives
    // users the option of using either type arguments or a value
    // arguments, which often eliminates the need for decltype:
    static_assert(ct::arity<foo>() == 4, "");
    static_assert(ct::arity(foo{}) == 4, "");

    // Attentive readers might notice that the type of the foo{}
    // expression above is foo&&, rather than foo. Indeed,
    // CallableTraits is designed to also allow both ref-qualified
    // and cv-qualified arguments across the board:

    static_assert(ct::arity<foo&&>() == 4, "");

    // Now, if foo had an operator() overload with a && qualifier, taking
    // a different number of arguments, the above static assert would fail.

    // For consistency, we'll avoid the value-style overloads
    // for the remainder of this example (whenever possible).

    static_assert(ct::max_arity<foo>() == 4, "");
    static_assert(ct::min_arity<foo>() == 3, "");

    // a quick way to check for a void return type
    static_assert(ct::has_void_return<foo>(), "");

    // C-style variadics detection (e.g. an ellipses in a signature)
    static_assert(!ct::has_varargs<foo>(), "");

    int i = 0;

    // callable_traits::can_invoke allows us to preview whether
    // std::invoke would compile with the given arguments.
    static_assert(ct::can_invoke(foo{}, 0, 0, i), "");
    // no error:     std::invoke(foo{}, 0, 0, i);

    // This call returns std::false_type, because it's an illegal call.
    static_assert(!ct::can_invoke(foo{}, nullptr), "");
    // error:         std::invoke(foo{}, nullptr);

    // Note that since can_invoke models std::invoke,
    // only a value-style function is defined.

    // For function objects, the following checks are determined by the
    // function qualifiers on operator(), rather than the qualifiers on
    // of the type passed. This is done for consistency with member function
    // pointers, where the checks below would look at the function qualifiers
    // (rather than qualifiers on the pointer itself).
    static_assert(ct::is_const_member<foo>(), "");
    static_assert(!ct::is_volatile_member<foo>(), "");
    static_assert(!ct::is_reference_member<foo>(), "");
    static_assert(!ct::is_lvalue_reference_member<foo>(), "");
    static_assert(!ct::is_rvalue_reference_member<foo>(), "");

    // is_constexpr would return std::true_type if foo's operator() were constexpr.
    static_assert(!ct::is_constexpr<foo>(), "");

    // The same check can be performed using std::integral_constant
    // in conjunction with function addresses:
    using pmf = decltype(&foo::operator());
    using pmf_constant = std::integral_constant<pmf, &foo::operator()>;
    static_assert(!ct::is_constexpr<pmf_constant>(), "");

    // So that you don't have to scroll to the top to check,
    // here's the type of pmf for reference.
    using with_const = void (foo::*)(int, int&&, const int&, void*) const;
    static_assert(std::is_same<pmf, with_const>::value, "");

    // If you find yourself in the unfortunate-and-probably-avoidable
    // situation of needing to transform member function pointer
    // types, CallableTraits has all the tools you need to prolong
    // your sanity.

    // CallableTraits lets you manipulate qualifiers on PMF types.
    // To remove const:
    using mutable_pmf = ct::remove_member_const<pmf>;
    using without_const = void (foo::*)(int, int&&, const int&, void*) /*no const!*/;
    static_assert(std::is_same<mutable_pmf, without_const>::value, "");

    // To add an rvalue qualifier:
    using rvalue_pmf = ct::add_member_rvalue_reference<pmf>;
    using with_rvalue = void (foo::*)(int, int&&, const int&, void*) const &&;
    static_assert(std::is_same<rvalue_pmf, with_rvalue>::value, "");

    // Just like std::add_rvalue_reference, callable_traits::add_member_rvalue_reference
    // follows C++11 reference collapsing rules. While remove_member_const
    // and add_member_rvalue_reference are somewhat clumsy names, they are the best
    // the best the author could provide while still allowing both terseness
    // and grep-ability against std::remove_const, etc. in <type_traits>.
    // Naturally, CallableTraits provides similar tools for the other C++
    // function qualifiers. Head to the reference section of this documentation
    // for more examples.

    // To remove a member pointer:
    using fn = ct::remove_member_pointer<pmf>;
    using expected_fn = void (int, int&&, const int&, void*) const;
    static_assert(std::is_same<fn, expected_fn>::value, "");

    // We just created an abominable function type - notice the const
    // qualifier! namespace_scopedremove_member_const accepts abominable
    // types too (and so does any feature where it is legal to do so):
    using not_abominable = ct::remove_member_const<fn>;
    using expected_fn2 = void (int, int&&, const int&, void*);
    static_assert(std::is_same<not_abominable, expected_fn2>::value, "");
}

Next