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.