C++20 - Overloaded Call Operators
Overload sets are one of the things that are tricky to deal with in the C++ type system. There isn’t really a good way to query or process them.
Note
Even with C++26 it isn’t trivial to process an overload set when you have ADL (argument dependent lookup) to contend with.
But it is not beyond the realms of possibility that we would need to handle this case. In Unreal Engine 4 there were a couple of uses of objects with multiple call operator overloads being used in algorithms, and I am sure UE isn’t the only one. So let’s have a look at the fundamental problem with dealing with an overload set.
Step 0: The fundamental problem with overload sets
Given the following callables:
struct RegularCallable
{
void operator()(int, float, double, bool){}
};
struct TemplatedCallOperator
{
void operator()(auto, auto, auto, int, float){}
};we can quite easily get the address of the function, which lets us get the member function pointer type, which lets us do our introspection:
int main()
{
auto regularCallableAddress = &RegularCallable::operator();
auto templatedCallOperatorAddress = &TemplatedCallOperator::template operator()<double, int*, bool>;
}We can’t actually do this with overload sets. Trying to get the address of the call operator can’t be done because the compiler doesn’t know which one you want. And unlike in the case of the templated call operator, there is no mechanism to provide your argument types to find out what overload the compiler would pick.
So what can we do?
Step 1: “Go fish”
We can’t get the compiler to give us the function signature of the chosen overload, but we can do this…
struct OverloadedFoo
{
void Foo(int) const;
void Foo();
};
int main()
{
[[maybe_unused]] auto valid1 = static_cast<void(OverloadedFoo::*)(int) const>(&OverloadedFoo::Foo);
[[maybe_unused]] auto valid2 = static_cast<void(OverloadedFoo::*)()>(&OverloadedFoo::Foo);
[[maybe_unused]] auto notValid = static_cast<void(OverloadedFoo::*)() const>(&OverloadedFoo::Foo);
}Casting an overloaded member function to a particular member function pointer is permitted. If the function overload exists, the cast is well formed, and you get your member function pointer.
If the function overload does not exist, the cast is ill-formed. Note how your cast has to match exactly to one of the overloads, so the only way you get a well formed cast is if you already know the exact function signature of the overload you want. Your ArgTypes will not necessarily provide you with a well-formed cast.
This is the best we can do, so let’s make lemonade SFINAE Concepts!
template<typename FunctorType, typename MemFuncPtrType>
concept HasCallOperatorOverload = requires
{
static_cast<MemFuncPtrType>(&FunctorType::operator());
};
template<typename FunctorType, typename... ParamTypes>
concept HasExactOverloadForParamsType =
std::invocable<FunctorType, ParamTypes...> &&
(
HasCallOperatorOverload<FunctorType, std::invoke_result_t<FunctorType, ParamTypes...>(FunctorType::*)(ParamTypes...)> ||
HasCallOperatorOverload<FunctorType, std::invoke_result_t<FunctorType, ParamTypes...>(FunctorType::*)(ParamTypes...) const>
);
namespace HasExactOverloadTests
{
struct CallableOverload
{
void operator()(Small){}
void operator()(Large){}
Small operator()(SmallNotTrivial&) const { return Small{}; }
};
static_assert(HasExactOverloadForParamsType<CallableOverload, Small>);
static_assert(HasExactOverloadForParamsType<CallableOverload, Large>);
static_assert(HasExactOverloadForParamsType<CallableOverload, SmallNotTrivial&>);
static_assert(!HasExactOverloadForParamsType<CallableOverload, Small&>);
}This is a concept that can evaluate whether a call operator has the function signature that you specify.
Note
HasExactOverloadForParamsType asks the question for const and non-const versions of the function signature. Obviously we could also add in every CV, ref, and noexcept combination here… But I am not going to bother.
Feel free to add those if you wish :)
Step 2: Building up our Type List
It has become clear that the strategy for solving this issue is to do the following:
- Generate a list of type lists for each permutation of possible qualifiers for each argument
- For each type list in this list of type lists, ask if it is a valid overload
- If it is valid, then validate that the parameters won’t lead to expensive copies
- If it is not valid, carry on to the next permutation
Both of these points involve manipulating type lists in some way. Currently, we can’t do that so I think the best thing we can do now is to start building up functionality for TypeList
Step 2.1: TypeList indexer
First thing we are likely to want is a way to index into our TypeList and get the corresponding type.
Note
There is a more efficient way of doing this than the way I am going to show. The following method is just quick to write, and “does the job”.
The better way to solve this is by making use of function overloading (ironically enough).
If you want to see how, have a look at tuplet. It is currently my favourite tuple implementation. MUCH quicker to compile than std::tuple.
template<u32 Index, typename List>
using GetType = decltype(
[]()
{
auto func =
[]<u32 InIndex, typename T, typename... InTypes>(auto InThis, TypeList<T, InTypes...>)
{
if constexpr (InIndex == 0)
{
return std::type_identity<T>{};
}
else
{
return InThis.template operator()<InIndex - 1>(InThis, TypeList<InTypes...>{});
}
};
return func.template operator()<Index>(func, List{});
}()
)::type;
namespace GetTypeTests
{
static_assert(std::is_same_v<int, GetType<0, TypeList<int, double, bool>>>);
static_assert(std::is_same_v<double, GetType<1, TypeList<int, double, bool>>>);
static_assert(std::is_same_v<bool, GetType<2, TypeList<int, double, bool>>>);
}We have gone with good old recursion for this. But probably not the type of recursion that people familiar with template meta-programming would be familiar with.
I have found that from C++20, most template meta-programming tasks can be solved with constexpr lambdas. And the nice thing about constexpr functions in general is that they look like actual C++ code!
Some elements to point out here:
- Nested lambda -> we are using a nested lambda. This allows us to manipulate the variadic pack. Notice how we take in a
TypeList<T, InTypes...>, and the next recursive call is invoked withTypeList<InTypes...>. This is the mechanism for iterating through the type list. It also allows us to do recursion within this very limited scope. - The first argument is the lambda itself -> in C++20 this is the only way to do recursion with a lambda. You need to pass the lambda to itself and keep passing it through. If we were to up the standard version to C++23 we get “deducing this”, which makes recursive lambdas much cleaner
decltype([]() -> std::type_identity<T>{}())::type-> Obviously we can’t return a type from a function. But what we can do is return an instance ofstd::type_identity. Then we can get thedecltypeof that function call and grab thetypealias from that return type.
Step 2.2: TypeList size
This is a much easier step than the last. To get the size or length of a TypeList all that is required is the following:
template<typename... Ts>
constexpr u32 GetSize(TypeList<Ts...>)
{
return sizeof...(Ts);
}And that is all we really need from our TypeList from here on out!
Step 3: Generating the list of TypeLists
We need to check every possible permutation of type and qualifier in the parameter list. If we say that we only care about const and l-value reference qualifiers, then that gives us 4 possible types per parameter:
template<typename T>
using Qualifiers = TypeList<const T&, T&, const T, T>;The issue with this is that the number of possible permutations that we will need to check for is , where N is the number of parameters. With 2 parameters that is 16 permutations which isn’t too bad. But that number starts getting a little scary when we hit 4 parameters (256 permutations).
In the rest of this section, I am going to solve this problem exhaustively, but it is probably reasonable to only care about the cases where all parameters have the same qualifier. In that case the complexity drops from to which is much more manageable.
Step 3.1: Generating all permutations
Actually generating all permutations isn’t that difficult:
template<u32 NumArgs>
constexpr auto CreateOrderedQualifierCartesianProduct()
{
constexpr u32 numQuals = GetSize(Qualifiers<int>{});
constexpr auto TotalPermutations =
[]()
{
u32 ret = 1;
for (u32 i = 0; i < NumArgs; ++i)
{
ret *= numQuals;
}
return ret;
}();
std::array<std::array<u32, NumArgs>, TotalPermutations> permutations{};
for (u32 i = 0; i < TotalPermutations; ++i)
{
u32 numToDecompose = i;
for (u32 j = 0; j < NumArgs; ++j)
{
permutations[i][j] = numToDecompose % numQuals;
numToDecompose /= numQuals;
}
}
return permutations;
}TotalPermutations is initialized by doing the calculation for . Then we create a std::array that will store all of the permutations possible. Then we construct each permutation by treating each iteration i as a base numQuals number, which we can decompose into numArgs digits.
This is the same algorithm we have all used to decompose a number into the digits that make it up. For example, if we are trying to find every permutation of 2 digits where each digit could be a number in the range [0,9]. In this case we would have 100 () permutations, and those permutations would be the numbers [0,99]. This is why the same algorithm to inspect each digit:
int numToDecompose = 46;
for (int i = 0; i < 2; ++i)
{
std::cout << numToDecompose % 10;
numToDecompose /= 10;
}will yield us the correct result when we just change the base from 10 to the number of qualifiers.
Step 3.2: Prioritizing permutations
The code above will yield all permutations. However, we want to find the correct permutation as soon as possible. The fewer iterations we have to check, the faster our compile time. So we need to order this list in some way.
Note how the Qualifiers TypeList is constructed:
template<typename T>
using Qualifiers = TypeList<const T&, T&, const T, T>;These have been ordered such that the most likely parameter types to pass the test have the smallest indices in the array. const T& is guaranteed to pass our test. There is a chance that T will fail our test. With this knowledge we can put together a little sort for our permutations:
std::ranges::sort(permutations,
[&](const std::array<u32, NumArgs>& InLeft, const std::array<u32, NumArgs>& InRight)
{
auto scoreFunction =
[](const std::array<u32, NumArgs>& InPerm)
{
const bool allEqual = std::ranges::adjacent_find(InPerm, std::not_equal_to{}) == InPerm.cend();
if (allEqual)
{
return InPerm[0];
}
else
{
return numQuals +
[&]()
{
u32 ret = 0;
for (const auto val : InPerm)
{
ret += val * val;
}
return ret;
}();
}
};
return scoreFunction(InLeft) < scoreFunction(InRight);
});C++20 brought ranges, and it also constexpr’d all the things!. This means we can use std::sort and std::ranges::sort in a constexpr function which is fantastic.
The key thing to note here is that we define a score function which takes 2 paths:
allEqualpath -> If all the indices in a permutation are the same (e.g. std::array{0,0,0,0} or std::array{2,2,2,2}), then its score is the same as the index. This is to ensure that they are at the front. I feel it is more likely that a function will be written asconst T&, const U&compared toT, U&.- different qualifiers path -> the score of these permutations is
numQuals + sumOfSquaredIndices.numQualsso that we purposefully put these behind any that have the same qualifier for all parameters. Squared indices rather than just the sum of indices so that we are biased towards reference parameters which are most likely to succeed the test.
If you click the link in the sort snippet, you will find that the example does not compile because of this:
template<auto Val>
struct PrintVal;
constexpr PrintVal<CreateOrderedQualifierCartesianProduct<2>()> ValidateProduct;This allows you to see the series generated in the compile error:

As you can see, all the similar permutations are at the start, and then we go through all the permutations that involve a reference next, and so on.
Step 4: Detecting callables without overloads
We have got a lot of machinery in place to start processing function overloads. However, we haven’t actually created a way to detect if a callable has multiple overloads or not. So let’s fix that now!
The first thing we need is to know if we have a callable with a single call operator overload:
template<typename CallableType>
concept SingleCallOperatorType = requires
{
&std::remove_reference_t<CallableType>::operator();
};
template<typename CallableType>
requires std::is_class_v<CallableType> && SingleCallOperatorType<CallableType>
struct FunctionTraits<CallableType> : FunctionTraits<decltype(&std::remove_reference_t<CallableType>::operator())>{};To do this we just create a concept and if we can get the call operator of it, then it is not overloaded, the expression is well-formed, and so the concept returns true. If the expression is not well-formed then the concept returns false, indicating that the type is either:
- Not a callable
- Is a callable with multiple call operator overloads.
This is important for our FunctionTraits specialization for callables. We need to constrain FunctionTraits to callables that have a single call operator because, as we have learned in this article, a type trait is not enough to reflect on the parameter types of a callable with multiple call overloads.
The next thing to do is to create this concept:
template<typename Callable>
concept HasFunctionTraits = requires
{
typename FunctionTraits<Callable>::ParamList;
};The purpose of this concept is to determine if a type is either a function type, or a callable type with a single call operator overload. This is important for the next step…
Step 5: Handling callables with multiple overloads in HasValidFunctorParams
Now, we can use our new concepts to branch on whether we can handle a callable using FunctionTraits or not, so let’s update HasValidFunctorParams with this new functionality, and add some new tests to make sure we are getting the behaviour we want:
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 if constexpr (HasFunctionTraits<FuncType>)
{
using ParamTypes = FunctionTraits<FuncType>::ParamList;
return AllOf(ParamTypes{},
[]<typename T>()
{
return IsValid<T>();
});
}
else
{
return true;
}
}();
namespace OverloadedCallables
{
struct OverloadedByVal
{
void operator()(Small, Small){}
void operator()(Large, Large){}
void operator()(SmallNotTrivial, SmallNotTrivial){}
};
struct OverloadedByRef
{
void operator()(Small&, Small&){}
void operator()(Large&, Large&){}
void operator()(SmallNotTrivial&, SmallNotTrivial&){}
};
static_assert(HasValidFunctorParams<OverloadedByVal, Small, Small>);
static_assert(!HasValidFunctorParams<OverloadedByVal, Large, Large>);
static_assert(!HasValidFunctorParams<OverloadedByVal, SmallNotTrivial, SmallNotTrivial>);
static_assert(HasValidFunctorParams<OverloadedByRef, Small, Small>);
static_assert(HasValidFunctorParams<OverloadedByRef, Large, Large>);
static_assert(HasValidFunctorParams<OverloadedByRef, SmallNotTrivial, SmallNotTrivial>);
}You will note that 2 of these tests are failing, and for good reason. We are returning true in the “multiple call operator overloads” case. That is fine for now. This at least tells us that we are able to correctly categorize what kind of callable a type is.
The next steps will focus on making these 2 failing tests pass by actually doing more than return true in the overloaded case.
Step 6: Turning permutation index lists into TypeLists
CreateOrderedQualifierCartesianProduct does not produce TypeLists that we can operate on. It only produces arrays of indices that refer to our Qualifiers TypeList. We need to be able to turn those arrays of indices into an actual TypeList:
template<auto IndexArray, typename BaseTypeList>
using GetQualifierPermutation = decltype(
[]()
{
constexpr auto IndexArraySize = IndexArray.size();
return
[]<u32... Indices, typename... Types>(std::integer_sequence<u32, Indices...>, TypeList<Types...>)
{
return std::type_identity<
TypeList< GetType<IndexArray[Indices], Qualifiers<Types>>... >
>{};
}(std::make_integer_sequence<u32, IndexArraySize>{}, BaseTypeList{});
}()
)::type;
namespace GetQualifierPermutationTests
{
static_assert(
std::is_same_v<
GetQualifierPermutation<
std::array<u32, 4>{0,1,2,3}, TypeList<bool, int, float, double>
>,
TypeList<const bool&, int&, const float, double>
>);
}This type alias is using more of the same tricks that we have seen before in the GetType section. The only interesting thing to point out is that we are using std::make_integer_sequence to index into our IndexArray to get the correct index to index into the Qualifiers type list.
You can see how it can be used in the test I wrote for it. An index of 0 maps to const T& so the output there is const bool&. An index of 2 maps to const T, so we get const float etc etc.
So given a list of base types and a corresponding index array, we can create a type list of types that have the specified qualifiers.
Step 7: Implementing ValidateOverloadedFunctor
Let’s just implement this step by step.
Step 7.1 The skeleton
This function will take in a type which is the functor type, and a variadic list of types that are the types of the arguments, and it will return a bool.
template<typename OverloadedFunctorType, typename... ArgTypes>
constexpr bool ValidateOverloadedFunctor()
{
return false;
}Easy peasy.
Step 7.2: Stripping ArgTypes of qualifiers
We need the base types of our ArgTypes so that we can apply the qualifiers we want. This is where std::remove_cvref comes in handy.
template<typename OverloadedFunctorType, typename... ArgTypes>
constexpr bool ValidateOverloadedFunctor()
{
using StrippedArgTypes = TypeList<std::remove_cvref_t<ArgTypes>...>;
return false;
}Step 7.3: Creating our list of permutation index lists
This step is fairly simple. We just need to generate our permutation list from the number of arguments that we have:
template<typename OverloadedFunctorType, typename... ArgTypes>
constexpr bool ValidateOverloadedFunctor()
{
using StrippedArgTypes = TypeList<std::remove_cvref_t<ArgTypes>...>;
constexpr auto permutations = CreateOrderedQualifierCartesianProduct<sizeof...(ArgTypes)>();
return false;
}We did all the hard work for this step earlier :)
Step 7.4: Looping through the permutation list
The key thing to note here is that we want to index into a TypeList using indices in a std::array. This fact prohibits us from using any kind of loop iteration (e.g. for). Why? Because no matter what you do, those indices can’t be constexpr, which is required to pass as a template argument to call GetType.
So we have two options:
- fold over an index sequence -> This is what we did with GetQualifierPermutation.
- recursion
For this section I am going with recursion so that we can early out when we find a result early. If we went the index sequence route, we would have to evaluate every permutation possible, which is something I want to avoid as much as possible. Additionally, I feel like using recursion is much more readable for most people.
Tip
Have a go at implementing it with the index sequence way! It could be fun :)
Lets first just define and call our recursive lambda to get us started:
template<typename OverloadedFunctorType, typename... ArgTypes>
constexpr bool ValidateOverloadedFunctor()
{
using StrippedArgTypes = TypeList<std::remove_cvref_t<ArgTypes>...>;
constexpr auto permutations = CreateOrderedQualifierCartesianProduct<sizeof...(ArgTypes)>();
auto loopFunc =
[&]<u32 permutationIndex>(auto InThis)
{
if constexpr (permutationIndex == permutations.size())
{
return false;
}
else
{
return InThis.template operator()<permutationIndex + 1>(InThis);
}
};
return loopFunc.template operator()<0>(loopFunc);
}This will recursively instantiate the loopFunc for every permutation in our permutations array from index 0, to permutations.size() -1. When it reaches permutations.size(), we stop the recursion and return false.
Note how the permutationIndex must be a template argument. This is because you can’t make parameters constexpr. If you want to pass something to a function and treat it in a constexpr manner, it must be passed in via a template argument.
Step 7.5: Validating a permutation type list
The next and final step is to turn our index array in each iteration into an actual TypeList and validate that TypeList
template<typename OverloadedFunctorType, typename... ArgTypes>
constexpr bool ValidateOverloadedFunctor()
{
using StrippedArgTypes = TypeList<std::remove_cvref_t<ArgTypes>...>;
constexpr auto permutations = CreateOrderedQualifierCartesianProduct<sizeof...(ArgTypes)>();
auto loopFunc =
[&]<u32 permutationIndex>(auto InThis)
{
if constexpr (permutationIndex == permutations.size())
{
return false;
}
else
{
using PermutationList = GetQualifierPermutation<permutations[permutationIndex], StrippedArgTypes>;
if constexpr (
[]<typename... ParamTypes>(TypeList<ParamTypes...>)
{
return HasExactOverloadForParamsType<OverloadedFunctorType, ParamTypes...>;
}(PermutationList{}))
{
return AllOf(PermutationList{},
[]<typename T>()
{
return IsValid<T>();
});
}
else
{
return InThis.template operator()<permutationIndex + 1>(InThis);
}
}
};
return loopFunc.template operator()<0>(loopFunc);
}
namespace ValidateOverloadedFunctorTests
{
struct CallableOverload
{
void operator()(Small){}
void operator()(Large){}
void operator()(const Small, const Small&){}
void operator()(const Large&, const Large&){}
};
static_assert(ValidateOverloadedFunctor<CallableOverload, Small>());
static_assert(!ValidateOverloadedFunctor<CallableOverload, Large>());
static_assert(ValidateOverloadedFunctor<CallableOverload, Small, Small>());
static_assert(ValidateOverloadedFunctor<CallableOverload, Large, Large>());
static_assert(!ValidateOverloadedFunctor<CallableOverload, Small, Large>());
}We use GetQualifierPermutation to create the TypeList then we must check if this is a valid overload that exists. We do this validation by using a lambda to introduce a variadic pack of types for our TypeList and then return the result of evaluating our HasExactOverloadForParamsType concept that we defined earlier.
If this returns true then we can just call AllOf like any other case in this article series.
Step 8: Putting it all together
We now have a function which can validate a callable that has multiple call operator overloads! That leaves us with only a couple of things to do!
- Actually call
ValidateOverloadedFunctorin our primary type trait - Uncomment our failing tests.
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 if constexpr (HasFunctionTraits<FuncType>)
{
using ParamTypes = FunctionTraits<FuncType>::ParamList;
return AllOf(ParamTypes{},
[]<typename T>()
{
return IsValid<T>();
});
}
else
{
return ValidateOverloadedFunctor<FuncType, ArgTypes...>();
}
}();
namespace OverloadedCallables
{
struct OverloadedByVal
{
void operator()(Small, Small){}
void operator()(Large, Large){}
void operator()(SmallNotTrivial, SmallNotTrivial){}
};
struct OverloadedByRef
{
void operator()(Small&, Small&){}
void operator()(Large&, Large&){}
void operator()(SmallNotTrivial&, SmallNotTrivial&){}
};
static_assert(HasValidFunctorParams<OverloadedByVal, Small, Small>);
static_assert(!HasValidFunctorParams<OverloadedByVal, Large, Large>);
static_assert(!HasValidFunctorParams<OverloadedByVal, SmallNotTrivial, SmallNotTrivial>);
static_assert(HasValidFunctorParams<OverloadedByRef, Small, Small>);
static_assert(HasValidFunctorParams<OverloadedByRef, Large, Large>);
static_assert(HasValidFunctorParams<OverloadedByRef, SmallNotTrivial, SmallNotTrivial>);
}Everything is now passing! Happy days!
Wrapping up
As you can see, supporting the multiple call operator case is a lot more involved than any other part of this series. But we got through it! While implementing this part we chatted about:
- Returning types from functions… kinda
std::integer_sequenceconstexpralgorithms and ranges- recursive lambdas
In the next and final part of our blog series, I am going to show you how C++26 reflection makes this entire exercise absolutely trivial.
Note
I have two other ideas for entries in this series that I might do… later.
- C++14 implementation -> Though if you think that this part of the series was diabolical, the C++14 version is an order of magnitude worse.
- Improving for compile time performance -> I will not shy away from the fact that this is likely not the most performant way to implement this. It could be fun to optimize this for better build times.
Addendum: Compiler bug
While writing this article, I found a compiler bug in MSVC. If you have seen any of my talks, you will know that I am a large advocate for making bug reports for your compiler if you find any issues. Or if the bug report already exists, I advocate for signal boosting that already existing bug report.
So I thought it would be good to go through the bug I found, and also to link the report here for anyone to up-vote it if they care enough about getting it fixed :)
I found this while writing the ValidateOverloadedFunctor function. Specifically this part:
using PermutationList = GetQualifierPermutation<permutations[permutationIndex], StrippedArgTypes>;
if constexpr (
[]<typename... ParamTypes>(TypeList<ParamTypes...>)
{
return HasExactOverloadForParamsType<OverloadedFunctorType, ParamTypes...>;
}(PermutationList{}))
{
return AllOf(PermutationList{},
[]<typename T>()
{
return IsValid<T>();
});
}Originally GetQualifierPermutation was simply a lambda execution like so:
using PermutationList = decltype(
[]<typename... ParamTypes>(TypeList<ParamTypes...>)
{
return std::type_identity<
TypeList<GetType<QualifierIndiceIndex, Qualifiers<ParamTypes>>...>
>{};
}(StrippedArgTypes{})
)::type;
if constexpr (
[]<typename... ParamTypes>(TypeList<ParamTypes...>)
{
return HasExactOverloadForParamsType<OverloadedFunctorType, ParamTypes...>;
}(PermutationList{}))
{
return AllOf(PermutationList{},
[]<typename T>()
{
return IsValid<T>();
});
}But MSVC did not like this and failed to compile with the following error:
<source>(150): error C2011: 'ValidateOverloadedFunctor::<lambda_1>::()::<lambda_1>': 'class' type redefinitionClang is perfectly happy with it. I worked out that extracting that lambda out into a global type alias worked around the compiler bug and carried on, but made a note of this issue so that I could come back later and investigate.
The first thing I like to do when I am thinking of submitting a bug report is reducing the example down as much as I possibly can. So I came up with this:
constexpr bool Foo()
{
return
[]()
{
using Type = decltype(
[]()
{
return 42;
}()
);
return [](){ return sizeof(Type) == 4; }();
}();
}It turns out that this bug has already been reported, and you can find that bug report here: Rejects valid lambda with lambda in decltype. I have up-voted this bug report in the hopes that it gets some attention and gets fixed. If you are someone who cares about getting this bug fixed, then I would encourage you to do the same!
While I was getting this example together, I stumbled across another compiler error with similar code:
#include <type_traits>
constexpr bool Foo()
{
return
[]()
{
using Type = decltype(
[]()
{
return std::type_identity<
int
>{};
}()
)::type;
return sizeof(Type) == 4;
}();
}This produces the following error:
error C2510: 'decltype': left of '::' must be a class/struct/unionInitially I thought that this error and the original error would likely share the same root cause… However, I kept playing with the code and now I am not as sure as I initially was…
The reason that I am not entirely sure it is the same bug is because this slight modification compiles:
#include <type_traits>
constexpr bool Foo()
{
return
[]()
{
using Type = decltype(
[]()
{
return std::type_identity<
int
>{};
}()
);
using OtherType = Type::type;
return sizeof(OtherType) == 4;
}();
}Note that in this case I am getting the type alias of std::type_identity in a later statement. This compiles just fine. The reason that this example gives me doubt that they are the same bug is that the error:
error C2510: 'decltype': left of '::' must be a class/struct/unionimplied that the parser did not believe that the result of a decltype expression could be a class type. But in this example, it is perfectly fine with that idea because it is able to successfully bind the result of decltype to a type alias and then treat that type alias like a type later.
Both of these bugs definitely seem to come down to MSVC’s lambda parsing logic, specifically in the case of nested lambdas. And the nested lambda appears to need to be inside a decltype expression. It is likely that both of these bugs stem from the same root cause.
But, I am not a compiler engineer, and I have no idea what is going on in MSVC, so I thought I might as well create this bug report in the event that it is actually a subtly different bug.
You can find my new bug report here: MSVC rejects nested lambda decltype(lambda expression)::type alias in using declaration.
Important
I made this bug report while preparing this blog series, and the MSVC team reported fixing it within 2 days! I did not expect them to fix it that quickly!