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'
}
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.
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
I would like to be able to initialize my objects with a brace-init-list:
#include <initializer_list>
template <class T>
struct S {
T v[5];
S(std::initializer_list<T> l) {
int ind = 0;
for (auto &&i: l){
v[ind] = i;
ind++;
}
}
};
int main()
{
S<int> s = {1, 2, 3, 4, 5};
}
As I found out here: https://en.cppreference.com/w/cpp/utility/initializer_list, it is necessary to use the standard library for this.
But that is strange for me. I would suggest that such initialization is a part of the C++ syntax.
Is it possible to create a constructor without use of std:initializer_list?
Edit:
It could be useful for programming embedding devices where standard library is not available (e.g. Arduino AVR).
As a work around one could do something like
#include <iostream>
template <class T>
struct S {
T v[5];
template<typename... Args>
requires (std::same_as<T, Args> && ...)
S(Args... args) {
static_assert(sizeof...(args) <= 5);
int ind = 0;
((v[ind++] = args), ...);
}
};
int main()
{
S<int> s = {1, 2, 3, 4, 5};
for ( auto e : s.v ) {
std::cout << e << ' ';
}
}
live on godbolt
As RemyLebeau pointed out in the comments a constructor like S(T,T,T,T,T) is needed. With a variadic constructor template and C++17 fold expressions you can make the compiler provide a constructor of that form for each number of arguments.
Note that the requires constraint (C++20) is important if you want other constructors in that class. It constraints the template the form S(T,T,T,...) and will not take part in overload resolution for arguments, that are not all of type T. It should also be possible to achieve that pre C++20 with std::enable_if*.
Also remember that the compiler will instantiate a constructor with matching signature for each call with a different number of arguments. Thus this could result in code bloat if used often and with many different numbers of arguments in the braces.
* like this for example:
template<typename... Args,
typename std::enable_if_t<(std::is_same_v<T, Args> && ...), bool> = true>
S(Args... args) {
//...
}
In the std::optional::emplace docs there is an overload that accepts std::initializer_list:
template< class U, class... Args >
T& emplace( std::initializer_list<U> ilist, Args&&... args );
provided that
std::is_constructible<T, std::initializer_list&, Args&&...>::value is true
I thought that it might be used to emplace POD types, but apparently this is not how it works (in other SO topics it was explained that emplace functions are using () syntax instead of {}):
struct A
{
int x;
int y;
int z;
};
int main()
{
A normalA{1, 2, 3}; // this is OK
std::cout << std::is_constructible<A, std::initializer_list<int>&, int, int, int>::value << std::endl; // false
std::cout << std::is_constructible<A, std::initializer_list<int>&>::value << std::endl; // false
std::optional<A> optA;
// optA.emplace({1, 2, 3}); // this is NOK
optA.emplace(A{1, 2, 3}); // I can walk it around with copy-ctor
}
I can write the constructor accepting initializer_list:
struct B
{
B(std::initializer_list<int> l) {/* some impl here */}
int x;
int y;
int z;
};
and then call emplace like this:
std::optional<B> optB;
optB.emplace({1, 2, 3});
but shouldn't first emplace overload T& emplace( Args&&... args ); be enough for that?
I thought that it might be useful for the array types, but std::optional<int[]> xxx; does not compile anyway.
Can you please provide some example where second std::optional::emplace overload is used.
but shouldn't first emplace overload T& emplace( Args&&... args ); be enough for that?
It isn't because a braced-init-list, i.e. {1, 2, 3} has no type. Because it has no type, there is nothing to compiler can do to deduce what Args should be. We need to have an overload that explicitly takes a std::initializer_list so that we can avoid the compiler not being able to deduce what the braced-init-list should be considered as.
I would like to initialize a class with a mix of basic types, arrays and sub-structures like
A a { 1, // int
1.2, // double
"Hello", // const char *
{ 1, 2, 3 }, // array<int>
{ 1.2, 2.4 } // array<double>
{ 1, 1.2, "Hello" } // sub-structure of any types
};
where the sub-structure should be able to contain nested sub-structures as well.
For the basic types I can do that with variadic templates:
#include <iostream>
class A {
public:
template <class T, class... Args>
void print(T v) {
std::cout << v << std::endl;
}
template <class T, class... Args>
void print(T v, Args... args) {
std::cout << v << std::endl;
print(args...);
};
template <class T, class... Args>
A(T v, Args... args) {
print(v, args...);
};
};
int main() {
A a { 1,
1.2,
"Hello",
};
}
which correctly produces
1
1.2
Hello
But I fail for arrays and sub-structures. I'm restricted to C++11. Any help highly appreciated.
{/*..*/} has no type, and so cannot be deduced in template except for std::initializer_list<T> or C-array T(&)[N].
So for variadic template, as you cannot provide all those combination of overloads, you have to provide type, and as you are using template constructor, only in the type itself:
A a { 1, // int
1.2, // double
"Hello", // const char *
std::array<int, 3>{1, 2, 3 }, // array<int>
std::array<double, 2>{ 1.2, 2.4 } // array<double>
MyClass{ 1, 1.2, "Hello" } // sub-structure of any types
};
C++17, with CTAD, allows std::array{1, 2, 3} instead of std::array<int, 3>{1, 2, 3}.
From cppreference:
A braced-init-list is not an expression and therefore has no type,
e.g. decltype({1,2}) is ill-formed. Having no type implies that
template type deduction cannot deduce a type that matches a
braced-init-list, so given the declaration template void
f(T); the expression f({1,2,3}) is ill-formed.
So, as #Jarod42 has mentioned, you need to explicitly provide the type. You can also use an auto variable beforehand to deduce the type (e.g. into std::initializer_list<int>) and then pass the variable into the constructor.
This won't fit in a comment so I post it as an answer. What you call a main structure isn't a structure (or type) at all but a form on initialization. Namely list initialization. The values between the braces are matched against your constructor and since you have a constructor with variadic template arguments, your code compiles. Another reason is, that you only provide literals, and literals have very well defined types: int, const char[N], float, ... . The compiler already knows those types without you having to provide a type.
Now add another level of braces, for example:
{ 1, 2, { 3, 4, 5 }, "test" }
According to the rules of list initialization (which is used because of your outer pair of braces), the compiler now looks for a constructor that matches this signature:
Ctor(int, int, <unknown>, const char*)
As you can see the compiler doesn't know what type {3, 4, 5} is so it tries to find a constructor that at least matches all other arguments. If it finds one, it tries to pass the <unknown> part to whatever type the matched overload has at that position in the parameter list. This will then trigger another list initialization on that specific parameter and the whole process repeats itself.
Lets take our example list from above
{ 1, 2, { 3, 4, 5 }, "test" }
and a set of constructors
Ctor(int, float*, int, const char*); // A
Ctor(int, int, int, const char*); // B
Ctor(int, int, std::array<int, 3>, const char*); // C
Now the compiler tries to match all known types to the imaginary signature we defined above (yes const char* is not the actual type of the literal but we ignore that here):
Ctor(int, int, <unknown>, const char*)
Option A is ruled out immediately, because the types of the 2nd argument do not match, so we're left with B and C. For B and C both signatures match the known types, so we're left figuring out what <unknown> could be. B offers us an int and C std::array<int, 3>. Lets check out B: can we do int x{3, 4, 5} (this is a simplification and just for illustration purposes)? Turns out C++ doesn't allow us to do that, so we discard B as an option. Next is C: can we do std::array<int, 3> x{3, 4, 5}. That looks right, C++ allows us to do that. We found a possible match for <unknown> and the compiler selects C as the constructor to be invoked. Great!
Now lets declare a variadic template constructor
template <typename ...Args>
Ctor(Args&&... args);
and match that against our list. Since we have template parameters, we basically tell the compiler: take whatever type the argument gives us. So we do that. The first two and the last arguments are easy, they're just literals so we have
template <typename Arg3>
Ctor(int, int, Arg3&& arg3, const char*);
Now we try out our nested list. The argument is <unknown> because the compiler couldn't figure out what {3, 4, 5} is (we leave std::initializer_list out here as a special case). The template parameter says: take whatever the type of that argument is, and the argument type is <unknown>. Do you spot the problem?
I post this as an answer since I need code formatting. I fully understand that the way I want to do this does not work. I would not have started if I would not have seen this before:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
json j {
{ "Float", 1.2},
{ "Int", 2},
{ "List", { 1,2,3 }},
{ "Object", {
{ "First", 3 },
{ "Name", "Joe"}
}}
};
std::cout << j << std::endl;
}
This is from https://github.com/nlohmann/json and used varadic templates. It nicely produces this output:
{"Float":1.2,"Int":2,"List":[1,2,3],"Object":{"First":3,"Name":"Joe"}}
So there must be a way to make this work. Unfortunately my mind is too poor to understand the code used by that library.
I have the following templated code
#include <vector>
#include <array>
#include <iostream>
template<typename T1>
void foo(std::vector<T1> bar) {
std::cout << "GENERIC" << std::endl;
}
template<typename T1>
void foo(std::vector<std::vector<T1>> bar) {
std::cout << "SPECIFIC (vector)" << std::endl;
}
template<typename T1, int SIZE>
void foo(std::vector<std::array<T1, SIZE>> bar) {
std::cout << "SPECIFIC (array)" << std::endl;
}
int main() {
std::vector<std::vector<int>> a(2, std::vector<int> { 1, 2, 3});
std::vector<std::array<int, 3>> b(2, std::array<int, 3> {4, 5, 6});
foo(a);
foo(b);
}
which produces
SPECIFIC (vector)
GENERIC
I'm wondering why the vector-of-vector version is called with the specific template, but the vector-of-array version is called with the generic?
template<typename T1, size_t SIZE>
void foo(std::vector<std::array<T1, SIZE>> bar) {
std::cout << "SPECIFIC (array)" << std::endl;
}
You should use std::size_t instead of int.
run here
Edit :
Actually, your comments and my intuition about the code led me to dig into the topic. At first glance, a standard developer ( like me ) expect compiler to convert int to std::size_t(because they are both integral type and implicitly converting is very trivial) and select void foo(std::vector<std::array<T1, SIZE>> bar) as best specialization. So while reading template argument deduction page i found this :
If a non-type template parameter is used in the parameter list, and
the corresponding template argument is deduced, the type of the
deduced template argument ( as specified in its enclosing template
parameter list, meaning references are preserved) must match the type
of the non-type template parameter exactly, except that cv-qualifiers
are dropped, and except where the template argument is deduced from an
array bound—in that case any integral type is allowed, even bool
though it would always become true:
As always, of course, you must read few more times than once to understand what it means :)
So an interesting result comes out.
Already our desired specialization is not selected but if the compiler had been forced to select, it would be an error.
template<typename T1, int SIZE>
void foo(std::vector<std::array<T1, SIZE>> bar) {
std::cout << "SPECIFIC (array)" << std::endl;
}
int main() {
std::vector<std::array<int, 3>> b(2, std::array<int, 3> {4, 5, 6});
foo(b); // P = std::vector<std::array<int,(int)SIZE>
// A = std::vector<std::array<int,(unsigned_long)SIZE>>
// error: deduced non-type template argument does not have the same
// type as its corresponding template argument */
}
run code
Another interesting thing is :
If the non-type template argument had not been deduced, there would be no restriction which forces argument and template types to be same.
#include <vector>
#include <array>
#include <iostream>
template<typename T1, int SIZE>
void foo(std::vector<std::array<T1, SIZE>> bar) {
std::cout << "SPECIFIC (array)" << std::endl;
}
int main() {
std::vector<std::array<int, 3>> b(2, std::array<int, 3> {4, 5, 6});
foo<int,3>(b);
}
run code
I think this is simply due to one line from [temp.deduct.call]/4
In general, the deduction process attempts to find template argument values that will make the deduced A identical to A
To clarify, A means the parameter, from [temp.deduct.call]/1
...template argument deduction with the type of the corresponding argument of the call (call it A)...
As has already been pointed out, changing template<typename T1, int SIZE> to template<typename T1, size_t SIZE> fixes the issue you are seeing. As stated in [temp.deduct.call]/4, the compiler is looking to deduce an A that is identical to A. Since an std::array has template arguments <class T, size_t N> (from [array.syn]), it's second parameter is in fact size_t, not int.
Therefore, for the template deduction, your generic function of template<typename T1> is able to match exactly the type of A, where-as your specialized template<typename T1, int SIZE> is not an exact match. I believe MSVC is incorrect in its deduction.
I would like to have a class like this:
template<typename T>
struct Foo {
T* data_;
template<typename... Ts, std::enable_if<std::is_same<T,Ts>...>...>
explicit Foo(Ts...ts) : data_{ ts... } {}
};
However; something with the syntax is wrong, and I'm not sure if you can set parameters into a pointer directly like this upon initialization.
What I would like for this to do is simply this:
Foo<int> f1{ 1, 3, 5, 7 }; // Or
// Foo<int> f1( 1, 3, 5 7 );
// f1.data_[0] = 1
// f1.data_[1] = 3
// f1.data_[2] = 5
// f1.data_[3] = 7
// f1.data_[4] = ... not our memory either garbage or undefined...
Foo<float> f2{ 3.5f, 7.2f, 9.8f }; // Or
// Foo<float> f2( 3.5f, 7.2f, 9.8f );
// f2.data_[0] = 3.5
// f2.data_[1] = 7.2
// f2.data_[2] = 9.8
// f2.data_[3] = ... not our memory
I would also like to have the constructor check to make sure that each and every parameter that is passed into the constructor is of type <T>; simply put for each Ts it must be a T.
I might be overthinking this but for the life of me I can not get this or something similar to compile. I don't know if it's within enable_if, is_same or through the class's initializer list and trying to store the contents into a pointer. I don't know if I should use an array of T instead but the array's size won't be known until the arguments are passed into the constructor. I'm also trying to do this without using a basic container such as std::vector; it's more for self education than practical source code. I just want to see how this could be done with raw pointers.
Edit
I've changed my class to something like this:
template<typename T>
struct Foo {
T* data_;
template<typename... Ts, std::enable_if_t<std::is_same<T, Ts...>::value>* = nullptr>
explicit Foo( const Ts&&... ts ) : data_{ std::move(ts)... } {}
};
And when trying to use it:
int a = 1, b = 3, c = 5, d = 7;
Foo<int> f1( a, b, c, d );
Foo<int> f2{ a, b, c, d };
I'm a little closer with this iteration; but they both give different compiler errors.
The first being: C2661: "No overloaded function takes 4 arguments"
And the second: C2440: "initializing, cannot convert from initializer list to Container, no constructor could take the source type, or constructor overload resolution was ambiguous."
Why not simply use a std::initialize_list:?
#include <iostream>
#include <type_traits>
#include <vector>
template <class T>
struct Foo
{
std::vector<T> data_;
explicit Foo(std::initializer_list<T> data) : data_(data)
{
std::cout << "1";
};
template <typename... Ts,
typename ENABLE=std::enable_if_t<(std::is_same_v<T,Ts> && ...)> >
explicit Foo(Ts... ts) : Foo(std::initializer_list<T>{ts...})
{
std::cout << "2";
}
};
int main()
{
Foo<int> f1{1, 3, 5, 7}; // prints 1
Foo<int> f2(1, 3, 5, 7); // prints 1 then 2
return 0;
}
If some Ts are different from T you will get a compile-time error.
With
gcc -std=c++17 prog.cpp
you get:
Foo<int> f1{1, 3, 5., 7};
error: narrowing conversion of ‘5.0e+0’ from ‘double’ to ‘int’ inside
{ } [-Wnarrowing] Foo f1{1, 3, 5., 7};
^
and
Foo<int> f2(1, 3, 5., 7);
you get
error: no matching function for call to ‘Foo::Foo(int, int,
double, int)’ Foo f2(1, 3, 5., 7);
^ note: candidate: ‘template Foo::Foo(Ts ...)’ explicit Foo(Ts... ts) :
Foo(std::initializer_list{ts...})
...
Update: if you really want to use something like raw pointer, here is a complete working example:
#include <iostream>
#include <memory>
#include <type_traits>
#include <vector>
template <class T>
struct Foo
{
size_t n_;
std::unique_ptr<T[]> data_;
explicit Foo(std::initializer_list<T> data) : n_(data.size()), data_(new T[n_])
{
std::copy(data.begin(), data.end(), data_.get());
std::cout << "1";
};
template <typename... Ts, typename ENABLE = std::enable_if_t<(std::is_same_v<T, Ts> && ...)> >
explicit Foo(Ts... ts) : Foo(std::initializer_list<T>{ts...})
{
std::cout << "2";
}
friend std::ostream& operator<<(std::ostream& out, const Foo<T>& toPrint)
{
for (size_t i = 0; i < toPrint.n_; i++)
std::cout << "\n" << toPrint.data_[i];
return out;
}
};
int main()
{
Foo<int> f1{1, 3, 5, 7}; // prints 1
Foo<int> f2(1, 3, 5, 7); // prints 1,2
std::cout << f1;
std::cout << f2;
return 0;
}
I let you replace unique_ptr by a raw pointer with all the extra work: delete[] etc...
std::is_same only compares two types, and you can't use pack expansions to declare multiple template parameters. That means you'll need to pull all of your std::is_same checks out into another check:
template <typename T, typename... Ts>
struct all_same : std::bool_constant<(std::is_same<T, Ts>::value && ...)> {};
template <typename T>
struct Foo
{
std::vector<T> data_;
template <typename... Ts, std::enable_if_t<all_same<T, std::decay_t<Ts>...>::value>* = nullptr>
Foo(Ts&&... ts)
: data_{std::forward<Ts>(ts)...}
{
}
};
Live Demo
You also need to allocate memory for your data_ array. Here I've used std::vector to take care of that allocation for me, but you could use new[] and delete[] to manage it yourself if you really want to.
enable_if and is_same won't store anything anywhere, they are only compile-time constructs and do not yield to any code in the binary executable.
Regardless of the syntax, what your code is essentially doing is trying to take the address of a constructor argument (which is a temporary). This will be a dangling pointer as soon as the constructor exits.
Either Foo owns the memory area and must allocate in constructor and delete in destructor (if any doubt: use std::vector!), or it aliases some external memory, and must receive a pointer to that memory.
Now regarding syntax:
std::is_same is a template that provides a value boolean constant and is to be used like so: std::is_same<T1, T2>::value. Alternatively you can use std::is_same_v<T1, T2>.
std::enable_if provides a type type member, only if the constant expression (1st template parameter) is true. Use it like std::enable_if<expr, T>::type. If expr is true, type is a typedef to T. Otherwise it is not defined and yields a substitution failure. Alternatively you can use std::enable_if_t<expr, T>
You can have a look here for a similar approach of yours.
But you can also simplify all this by using a member vector. In that case, the vector constructor ensures that all arguments have compatible types. Here is a complete example:
#include <string>
#include <vector>
#include <iostream>
#include <type_traits>
using namespace std;
template<typename T>
struct Foo {
vector<T> data_;
template<typename ...Ts>
explicit Foo(Ts... ts) : data_{ ts... } {}
void print() {
for (const auto &v : data_) {
cout << v << " ";
}
cout << endl;
}
};
int main() {
Foo<int> ints { 1, 2, 3, 4, 5 };
Foo<string> strings { "a", "b", "c", "d", "e"};
// Foo<string> incorrect { "a", 2, "c", 4, "e"};
ints.print();
strings.print();
// incorrect.print();
return 0;
}
The most idiomatic way to do this in C++17 is using std::cunjunction_v. It lazily evaluates subsequent values and allows you to avoid fold expressions. Also messages generated by the compiler are the same for both of casese you mentioned in edited piece of code.
Also, passing data by const-rvalue-ref makes no sense, since it is impossible to move data from const objects. Additionaly you were moving pack of arguments to a pointer. I didn't know what to do about it so just removed it.
Additionaly, take a look at the std::decay_t - without it it would not work, as sometimes Ts is deduced not as int but as const int & or int &. This leads to std::is_same_v being false.
The code below compiles fine at godbolt:
template<typename T>
struct Foo {
T* data_;
template<
typename... Ts,
std::enable_if_t<
std::conjunction_v<
std::is_same<T, std::decay_t<Ts>>...
>
> * = nullptr
>
explicit Foo( Ts&&... ts ) : data_{ } {}
};