Home > Software design >  std::initializer_list rvalue/literal as parameter of (variadic) template
std::initializer_list rvalue/literal as parameter of (variadic) template

Time:09-29

Please help me to understand why a std::initializer_list literal can't be deduced as a template parameter? AFAIK, there's no notion of an init-list literal in the language yet, but then why/how does this work?

auto il = { 1, 2, 3, 4, 5 };

Here is my code:

import <iostream>;
import <string>;
import <vector>;
    
    
template <typename T>
constexpr auto type_name() {
    std::string_view name, prefix, suffix;
#ifdef __clang__
    name = __PRETTY_FUNCTION__;
    prefix = "auto type_name() [T = ";
    suffix = "]";
#elif defined(__GNUC__)
    name = __PRETTY_FUNCTION__;
    prefix = "constexpr auto type_name() [with T = ";
    suffix = "]";
#elif defined(_MSC_VER)
    name = __FUNCSIG__;
    prefix = "auto __cdecl type_name<";
    suffix = ">(void)";
#endif
    name.remove_prefix(prefix.size());
    name.remove_suffix(suffix.size());
    return name;
}
    
template< typename T >
int SAI_BF_helper(T&&) { return 0; }
    
int SAI_BF_helper(int i) { return i; }
    
// function that take any number parameters of any type and then return sum of all ints using binary left folding expressions
template< typename ... Types >
 ¡int SumAllInts_BinaryLeftFold(Types ... args)
{
    return (0   ...   SAI_BF_helper(args));
}
    
template< typename T >
void PrintTypeName(T&& t)
{
    std::cout << type_name< decltype( std::forward< T >(t) )> () << std::endl;
}
    
// if this overload is removed then 'PrintTypeName({1,2,3});' code will not compile
template< typename T >
void PrintTypeName( std::initializer_list< T >&& t)
{
    std::cout << type_name< decltype(std::forward< std::initializer_list< T > >(t))>() << std::endl;
}
    
int main()
{
    std::vector< int > numbers{ 1, 2, 3 };
    auto il = { 1, 2, 3, 4, 5 }; 
    PrintTypeName(numbers); // output: class std::vector<int,class std::allocator<int> >&
    PrintTypeName(il); // output: class std::initializer_list<int>&
    PrintTypeName({1,2,3}); // output: class std::initializer_list<int>&&
    
    std::cout << SumAllInts_BinaryLeftFold() << std::endl; // 0 
    std::cout << SumAllInts_BinaryLeftFold("", 0, 1, 2.2, 'a', "char*", 10, std::string("str"), numbers, il) << std::endl; // 11
    //std::cout << SumAllInts_BinaryLeftFold( 1, {1, 2}) << std::endl; // MSVC error message: 'initializer list': is not a valid template argument for 'Types'
}

CodePudding user response:

The short answer: initializer lists (the language mechanic, not the type) are magic. No, really.

The C standard has explicit wording for initializer-list syntax to produce a std::initializer_list object in certain circumstances such as constructing an auto-parameter, but an initializer list expression is not itself always a std::initializer_list; it depends on the circumstances.

This distinction is needed because the same syntax may be used in place of implicit construction for cases like parameters. For example, consider the following code:

auto do_something(Person) -> void;

...

do_something({}); // Calls Person() -- not Person(std::initializer_list<U>)

In this, {} isn't meant to be a std::initializer_list object; it's an implicit default-construction of Person.

Because these cases exist, deducing the exact type of a brace-enclosed initializer list is not so straight-forward. For example:

template <typename T>
auto do_something(T) -> void;

do_something({1}) -> void;

In the above case, what should T be? Is it do_something<std::initializer_list<int>>? do_something<int> with int{1}? What if this were {1, 2U}? It can get complicated.

CodePudding user response:

There is no such thing as a "std::initializer_list literal". {1, 2, 3} is a braced-init-list; a syntactic construct with no type that can be used to initialize many types. There are a few specific situations (see below) where a std::initializer_list will be implicitly created from a braced-init-list, but they are not the same thing.

From cppreference :

A std::initializer_list object is automatically constructed when:

  • a braced-init-list is used to list-initialize an object, where the corresponding constructor accepts an std::initializer_list parameter
  • a braced-init-list is used as the right operand of assignment or as a function call argument, and the corresponding assignment operator/function accepts an std::initializer_list parameter
  • a braced-init-list is bound to auto, including in a ranged for loop

auto il = {1, 2, 3, 4, 5};

This is one of the situations when a std::initializer_list will be implicitly created from a braced-init-list. A braced-init-list is being bound to auto.

template <typename T>
void PrintTypeName(std::initializer_list<T>&& t)
{
    ...
}

PrintTypeName({1,2,3});

This is another of the specific situations where a braced-init-list is automatically converted to std::initializer_list. A braced-init-list is being used as a function call argument and the corresponding function accepts a std::initializer_list parameter.

template<typename T>
void PrintTypeName(T&& t)
{
    ...
}

PrintTypeName({1,2,3});

This is not one of the situations where a braced-init-list will automatically be converted to a std::initializer_list. This PrintTypeName does not specifically accept a std::initializer_list.

  • Related