Calling Initializer_List Constructor via Make_Unique/Make_Shared

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 news 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 ints", 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 5can 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 or 0 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 the Foos with {1} and {2}.

    Calling forwarding( {1}, {2} ) is not. It does not know at the time you call forwarding that the arguments will be Foos, 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 a T* 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



Leave a reply



Submit