Search code examples
c++lambdac++23

Why does a free function work but its equivalent in lambda fails to compile?


The following get function will return an array of strings using the variadic arguments. It also handles the case with providing integers by simply converting them. I provided two versions of this conversion function, one being a lambda and the other as a free function.

Can some please provide an explanation to why using a lambda fails to compile while a seemingly identical free function works fine. What changes would be required to make the lambda case work?

#include <iostream>
#include <string>
#include <vector>
#include <ranges>
#include <format>
#include <array>
#include <type_traits>

// free function converter
template <typename KEY>
auto convert_to_string_view(const KEY& key)
{
    if constexpr (std::is_integral_v<KEY> && !std::is_same_v<KEY, bool>)
        return std::to_string(key);
    else
        return key;
}

// make an array out of varargs
template <typename T, typename... Args>
inline std::array<std::common_type_t<Args...>, sizeof...(Args)>
 make_array_from_vars(const Args&... args)
    requires (std::is_constructible_v<std::common_type_t<Args...>, Args> && ...)
{
    return { args... };
}



template<typename... KEYS>
auto get(KEYS... keys) {

    // lambda 
    auto convert_to_sv = [](const auto& key){
            if constexpr (std::is_integral_v<decltype(key)> && !std::is_same_v<decltype(key), bool>) {
                return std::to_string(key); 
            }
            else {
                return key;
            }
        };
    return make_array_from_vars<std::string_view>(convert_to_string_view(keys)...);
// using the lambda
    //return make_array_from_vars<std::string_view>(convert_to_sv(keys)...); FAILS

}

int main() {
    auto d = std::string("blablabla");
    auto tuple_range = get(d.append("!asdsads"), "blas", 0, 0);
    std::cout << tuple_range[0] << std::endl;
    return 0;
}

Solution

  • The specific issue here is that your function template and your lambda are checking different things:

    template <typename KEY>
    auto convert_to_string_view(const KEY& key)
    {
        if constexpr (std::is_integral_v<KEY> && !std::is_same_v<KEY, bool>)
    

    vs

    auto convert_to_sv = [](const auto& key){
        if constexpr (std::is_integral_v<decltype(key)> && !std::is_same_v<decltype(key), bool>) {
    

    The function template is checking whether the type parameter KEY is integral and not bool. The lambda is doing the same check for decltype(key).

    Now, considering calling both with an int. The function template deduces KEY=int and checks int. The lambda implicitly deduces int as well, but decltype(key) isn't int - it's int const&, because that's the type of the variable. Indeed, integral_v<decltype(key)> is never true - because references aren't integral.

    To make them line up, you can introduce a template parameter and then do the same check:

    auto convert_to_sv = []<class K>(K const& key){
        if constexpr (std::is_integral_v<K> && !std::is_same_v<K, bool>) {
    

    This would now have identical meaning.


    In general the abbreviated function template syntax is only useful if you really actually don't need the type of the variable. Once you start needing it - getting it using decltype is now longer than writing a normal function template declaration to begin with.