Skip to content

C++20 - Callable Types

May 31, 2026

Function types are not the only types that we will want to inspect the parameters of. In fact, function types are likely the least likely type of object to be passed to an algorithm or some other type of function that takes a callable. In most cases people will pass an object of a callable type. And more specifically, it will usually be a lambda. Thankfully we can treat types with a call operator and lambdas as the same kind of thing here.

Note

Although in every implementation of C++ (that I know of) lambdas and types with call operator overloads can be treated exactly the same, they are not the same kind of thing. The standard has no opinion on how lambdas should be implemented. Therefore lambdas are technically a different kind of thing compared with callable types. This becomes quite apparent when we start playing with C++26 reflection…

Step 0: Does our type trait deal with callable types?

First thing to do is to add some tests for callable types. In the spirit of being as lazy as possible, I will just use lambdas for tests for this section. For all intents and purposes they are the same… in this scenario at least:

namespace Lambda
{
    using SmallByValue = decltype([](Small, Small){});
    using LargeByValue = decltype([](Large, Large){});
    using SmallNotTrivialByValue = decltype([](SmallNotTrivial, SmallNotTrivial){}); 

    using SmallByRef = decltype([](const Small&, const Small&){});
    using LargeByRef = decltype([](Large&, Large&){});
    using SmallNotTrivialByRef = decltype([](SmallNotTrivial&, SmallNotTrivial&){});

    static_assert(HasValidFunctorParams<SmallByValue, Small, Small>);
    static_assert(!HasValidFunctorParams<LargeByValue, Large, Large>);
    static_assert(!HasValidFunctorParams<SmallNotTrivialByValue, SmallNotTrivial, SmallNotTrivial>);

    static_assert(HasValidFunctorParams<SmallByRef, Small, Small>);
    static_assert(HasValidFunctorParams<LargeByRef, Large, Large>);
    static_assert(HasValidFunctorParams<SmallNotTrivialByRef, SmallNotTrivial, SmallNotTrivial>);
}

You will find that these tests are all failing to compile. Because we are passing in class types to our FunctionTraits type trait, and it only supports function types right now.

So the next step will be expanding our FunctionTraits to cope with callable types.

Step 1: Specializing for class types with call operators

The first step here is to handle the case of member function pointers in FunctionTraits:

template<typename RetType, typename ClassType, typename... ParamTypes>
struct FunctionTraits<RetType(ClassType::*)(ParamTypes...)> : FunctionTraits<RetType(ParamTypes...)>{};

This specialization simply matches the signature of a member function pointer and then delegates the parts of the signature that we care about up to the free function specialization. We could leave this here, and just make users of this trait be responsible for extracting the call operator member function out of their callable type. But of course that would be a little mean, so we need another specialization:

template<typename CallableType>
    requires std::is_class_v<CallableType>
struct FunctionTraits<CallableType> : FunctionTraits<decltype(&std::remove_reference_t<CallableType>::operator())>{};

There is a decent amount going on in this specialization so let’s have a tour around these 3 lines of code:

  1. requires std::is_class_v<CallableType> -> This constrains this specialization to only matching class types. And yes lambdas are seen as a class type by this type trait… even though they technically are not. This is a necessary component because without this constraint, there is no difference between this specialization and the primary template, so the compiler would see them as ambiguous. Clang emits this error for example:
<source>:36:8: error: class template partial specialization does not specialize any template argument; to define the primary template, remove the template argument list
   36 | struct FunctionTraits<CallableType> : FunctionTraits<decltype(&std::remove_reference_t<CallableType>::operator())>{};
      |        ^             ~~~~~~~~~~~~~~
  1. decltype(&std::remove_reference_t<CallableType>::operator()) -> This is how we extract the member function pointer type of the call operator. This reads as, “strip the reference off the CallableType if it exists, then get the address of its call operator, then get the type of that address”. That address is a member function pointer address so getting the type of it matches the member function specialization of FunctionTraits that we created above.

Now unfortunately if you follow the link in the last code snippet you will find that we are still getting compilation errors. Why?

Step 2: Specializing for qualified member functions

Lambdas are one of those special cases in C++ where the defaults are correct. One of those defaults is the default for the this pointer in a lambda. Lambda call operators are const by default. You have to explicitly mark the lambda as mutable to modify any of the captures. And as you can see, we have only specialized FunctionTraits for mutable member functions. This is the source of our compilation errors. The fix is as simple as another specialization:

template<typename RetType, typename ClassType, typename... ParamTypes>
struct FunctionTraits<RetType(ClassType::*)(ParamTypes...) const> : FunctionTraits<RetType(ParamTypes...)>{};

With this little addition we now have every static_assert passing!

Obviously stopping here means that we are not handling every possible member function. We are only handling const and non-const l-value referenced member functions. If you have a look at std::move_only_function and std::copyable_function you will find that they handle every combination of const, noexcept, and reference qualifier.

Note

Notice that std::move_only_function and std::copyable_function don’t support volatile qualifiers. Some of the rationale for this can be found in n4159

But I will not be that thorough in this article. I will be doing the minimum here to solve the majority of cases. If you want to be thorough and cover the cases that I am omitting, go for it!

Step 3: Templated call operator overloads

What we currently have will work for the vast majority of callables. But if someone decides to be lazy and provide a callable that looks like this:

auto myCallable = 
    [](const auto InThing)
    {
        return InThing.DoSomething();
    };

Then we are out of luck again. We do not handle call operator templates… yet.

Step 3.1: Adding new tests for call operator templates

First thing to do before we start implementing this is to add some new tests:

namespace Lambda
{
    using AutoByVal = decltype([](auto, auto){});
    using AutoByRef = decltype([](const auto&, const auto&){});

    static_assert(HasValidFunctorParams<AutoByVal, Small, Small>);
    static_assert(!HasValidFunctorParams<AutoByVal, Large, Large>);
    static_assert(!HasValidFunctorParams<AutoByVal, SmallNotTrivial, SmallNotTrivial>);

    static_assert(HasValidFunctorParams<AutoByRef, Small, Small>);
    static_assert(HasValidFunctorParams<AutoByRef, Large, Large>);
    static_assert(HasValidFunctorParams<AutoByRef, SmallNotTrivial, SmallNotTrivial>);
}

These tests are failing for now, but it won’t take much effort to get them passing :)

Step 3.2: Why are we passing in ArgTypes again?

You may have noticed that HasValidFunctorParams takes in the argument types that a call is made with:

template<typename FuncType, typename... ArgTypes>
inline constexpr bool HasValidFunctorParams = 
    []()
    {
        using ParamTypes = FunctionTraits<FuncType>::ParamList;

        return AllOf(ParamTypes{},
            []<typename T>()
            {
                return IsValid<T>();
            });
    }();

You may have also noticed here that we are never using those argument types. The reason for this is that we haven’t needed to use them yet. We have been able to unambiguously determine the function signature that we are inspecting just from FuncType. However, with call operator templates, that is not the case. We need these ArgTypes to instantiate the operator template.

Step 3.3: Detecting if a call operator is templated

The next step is to be able to determine if FuncType is a type that has a templated call operator or just a regular call operator. Thanks to concepts this is fairly easy:

template<typename CallableType, typename... Args>
concept InvocableTemplate = requires
{
    { &std::remove_reference_t<CallableType>::template operator()<Args...> };
};

namespace InvocableTemplateTests
{
    struct Empty{};

    struct Callable
    {
        void operator()(int){}
    };

    struct CallableTemplate
    {
        template<typename T>
        void operator()(T){}
    };

    static_assert(!InvocableTemplate<Empty>);
    static_assert(!InvocableTemplate<Callable, int>);
    static_assert(InvocableTemplate<CallableTemplate, int>);
}

All this concept is doing is seeing if the expression of getting the address of an instantiated call operator is valid. If it is well-formed, then the concept evaluates to true. If the expression is ill-formed, the concept returns false.

Step 3.4: Handling the call operator template case

Once we can detect a call operator template, we can branch on it quite easily:

template<typename FuncType, typename... ArgTypes>
inline constexpr bool HasValidFunctorParams = 
    []()
    {
        if constexpr (InvocableTemplate<FuncType, ArgTypes...>)
        {
            // Handle the call operator template case
        }
        else
        {
            using ParamTypes = FunctionTraits<FuncType>::ParamList;
            return AllOf(ParamTypes{},
                []<typename T>()
                {
                    return IsValid<T>();
                });
        }
    }();

Note

if constexpr and concepts make this code really nice. In the C++14 days, this would have taken a mess of SFINAE, std::void_t, and template specializations to do this very simple branch.

Handling the call operator template case is very similar to how we defined our concept:

template<typename FuncType, typename... ArgTypes>
inline constexpr bool HasValidFunctorParams = 
    []()
    {
        if constexpr (InvocableTemplate<FuncType, ArgTypes...>)
        {
            using InstantiatedFuncType = decltype(&std::remove_reference_t<FuncType>::template operator()<ArgTypes...>);
            using ParamTypes = FunctionTraits<InstantiatedFuncType>::ParamList;
            return AllOf(ParamTypes{},
                []<typename T>()
                {
                    return IsValid<T>();
                });
        }
        else
        {
            using ParamTypes = FunctionTraits<FuncType>::ParamList;
            return AllOf(ParamTypes{},
                []<typename T>()
                {
                    return IsValid<T>();
                });
        }
    }();

We simply instantiate the call operator function template with our ArgTypes and get the type of that member function signature. The rest is a copy and paste of the other branch essentially. I am happy leaving this as a copy and paste for now. But of course you could refactor this into another function/lambda to reduce repetition.

Wrapping Up

In this section we touched on the following topics:

  1. if constexpr
  2. concepts
  3. member function pointers
  4. lambdas

Our little type trait has gotten a lot more powerful now, and for the vast majority of cases, this is probably all you will ever need. However, if you work on very old and very large code bases (like me), you are likely to hit another edge case:

Callable with multiple call operator overloads
struct CallableOverload
{
    void operator()(Small){}
    void operator()(Large){}
};

This is a fairly gnarly topic, with a fairly gnarly solution. Please join me in the next article where we tackle that problem!

Last updated on