Calling initializer_list constructor via make_unique/make_shared
std::make_unique
is a function template which deduces the argument types which are passed to the object constructor. Unfortunately, braced lists are not deducible (with an exception for auto
declarations), and so you cannot instantiate the function template when that missing parameter type.
You can either not use std::make_unique
, but please don't go that route – you should avoid naked new
s as much as you can, for the children's sake. Or you can make the type deduction work by specifying the type:
std::make_unique<Foo>(std::initializer_list<std::string>({"Hello", "World"}))
std::make_unique<Foo, std::initializer_list<std::string>>({"Hello", "World"})
auto il = { "Hello"s, "World"s }; auto ptr = std::make_unique<Foo>(il);
The last option uses the special rule for auto
declarations, which (as I hinted above) do in fact deduce an std::initializer_list
.
std::initializer_list and std::make_shared: too many arguments ... 3 expected 0 provided
{1,2,3}
can be multiple things, and make_shared has no possibility of knowing what it is at the time parameter pack is expanded.
If you don't want to state the long std::initializer_list<int>{1,2,3}
explicitly, the easiest solutions would be:
a. shortening the type's name: using ints=std::initializer_list<int>;
b. wrapping the call:
auto make_shared_S(int x, double y, std::initializer_list<int> l)
{
return std::make_shared<S>(x, y, l);
}
Demo: https://godbolt.org/z/WErz87Ks4
make_unique with brace initialization
Some classes have different behavior with the 2 initialization styles. e.g.
std::vector<int> v1(1, 2); // 1 element with value 2
std::vector<int> v2{1, 2}; // 2 elements with value 1 & 2
There might not be enough reason to choose one prefer to another; I think the standard just choose one and state the decision explicitly.
As the workaround, you might want to implement your own make_unique
version. As you have showed, it's not a hard work.
Is there a reason why std::make_shared/std::make_unique don't use list initialization?
Specifically, what pitfalls can be in the list initialization solution?
All of the typical pitfalls of using list-initialization.
For example, the hiding of non-initializer_list constructors. What does make_shared<vector<int>>(5, 2)
do? If your answer is "constructs an array of 5 int
s", that's absolute correct... so long as make_shared
isn't using list-initialization. Because that changes the moment you do.
Note that suddenly changing this would break existing code, since right now all of the indirect initialization functions use constructor syntax. So you can't just change it willy-nilly and expect the world to keep working.
Plus one more unique to this case: the narrowing issue:
struct Agg
{
char c;
int i;
};
You can do Agg a{5, 1020};
to initialize this aggregate. But you could never do make_shared<Agg>(5, 1020)
. Why? Because the compiler can guarantee that the literal 5
can be converted to a char
with no loss of data. However, when you use indirect initialization like this, the literal 5
is template-deduced as int
. And the compiler cannot guarantee that any int
can be converted to a char
with no loss of data. This is called a "narrowing conversion" and is expressly forbidden in list initialization.
You would need to explicitly convert that 5
to a char
.
The standard library has an issue on this: LWG 2089. Though technically this issue talks about allocator::construct
, it should equally apply to all indirect initialization functions like make_X
and C++17's in-place constructors for any
/optional
/variant
.
why does it too follow same pattern?
It follows the same pattern because having two different functions that look almost identical that have radically and unexpectedly different behaviors would not be a good thing.
Note that C++20 resolves the aggregate part of this issue at least by making constructor-style syntax invoke aggregate initialization if the initializers would have been ill-formed for regular direct initialization. So if T
is some aggregate type (with no user-declared constructors), and T(args)
wouldn't invoke a copy/move constructor (the only constructors that take arguments which a type with no user-declared constructors could have), then the arguments will instead be used to attempt to aggregate initialize the structure.
Since allocator::construct
and other forms of forwarded initialization default to direct-initialization, this will let you initialize aggregates through forwarded initialization.
You still can't do other list-initialization stuff without explicitly using an initializer_list
at the call site. But that's probably for the best.
std::shared_ptr and initializer lists
Try this:
auto ptr = std::make_shared<Func>(std::initializer_list<std::string>{"foo", "bar", "baz"});
Clang is not willing to deduce the type of {"foo", "bar", "baz"}
. I'm currently not sure whether that is the way the language is supposed to work, or if we're looking at a compiler bug.
std::initializer_list not able to be deduced from brace-enclosed initializer list
make_unique
uses perfect forwarding.
Perfect forwarding is imperfect in the following ways:
It fails to forward initializer lists
It converts
NULL
or0
to an integer, which can then not be passed to a value of pointer type.It does not know what type its arguments will be, so you cannot do operations that require knowing their type. As an example:
struct Foo { int x; };
void some_funcion( Foo, Foo ) {};
template<class...Args>
decltype(auto) forwarding( Args&& ... args ) {
return some_function(std::forward<Args>(args)...);
}Calling
some_function( {1}, {2} )
is legal. It constructs theFoo
s with{1}
and{2}
.Calling
forwarding( {1}, {2} )
is not. It does not know at the time you callforwarding
that the arguments will beFoo
s, so it cannot construct it, and it cannot pass the construction-initializer-list through the code (as construction-lists are not variables or expressions).If you pass an overloaded function name, which overload cannot be worked out at the point of call. And a set of overloads is not a value, so you cannot perfect forward it through.
You cannot pass bitfields through.
It forces takes a reference to its arguments, even if the forwarded target does not. This "uses" some static const data in ways that can cause a program to be technically ill-formed.
A reference to an array of unknown size
T(&)[]
cannot be forwarded. You can call a function taking aT*
with it, however.
About half of these were taken from this comp.std.c++ thread, which I looked for once I remembered there were other issues I couldn't recall off the top of my head.
Related Topics
Why Do I Need Double Layer of Indirection for MACros
How Portable Is End Iterator Decrement
Inaccessible Direct Base' Caused by Multiple Inheritance
"Observable Behaviour" and Compiler Freedom to Eliminate/Transform Pieces C++ Code
Declare a Reference and Initialize Later
Do All Pointers Have the Same Size in C++
C++11 Static_Assert and Template Instantiation
Simple Glob in C++ on Unix System
Override a Member Function with Different Return Type
Template Specialization and Enable_If Problems
C++ Threads, Std::System_Error - Operation Not Permitted
What Would Be C++ Limitations Compared C Language
How to Truncate a Floating Point Number After a Certain Number of Decimal Places (No Rounding)
What Are the Differences Between -Std=C++11 and -Std=Gnu++11