Using C++ Aggregate Initialization in Std::Make_Shared

using c++ aggregate initialization in std::make_shared

You could create an adapter with a variadic constructor template to forward the arguments, something like:

template<class T>
struct aggregate_adapter : public T {
template<class... Args>
aggregate_adapter(Args&&... args) : T{ std::forward<Args>(args)... } {}
};

And then you can do:

auto foo = std::make_shared<aggregate_adapter<Foo>>("hello", 5, 'c');

Since aggregate_adapter<Foo> and Foo are related, foo is convertible to std::shared_ptr<Foo> as well.

Caveats


Unfortunately, the use of forwarding also makes it impossible to brace-init any of the members like std::make_shared<aggregate_adapter<Foo>>({'h','e','l','l','o'}, 5, 'c'); without specifying the type explicitly, but the same restriction applies to make_shared already.

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.

Is there any way to trick std::make_shared into using default initialization?

Create a derived class to enforce trivial construction.

struct D : T {
D() {} // Non-trivial constructor. Default-initialize T, which may be trivial.
};

Construct the derived class but assign it to the shared pointer you want.

std::shared_ptr< T > p = std::make_shared< D >();

Demo.

Note that this is type-safe with respect to the destructor. shared_ptr always performs type erasure and uses dynamic dispatch before the destructor call, even for simple POD objects.

Aggregate initialization

That's correct. Aggregate initialisation is only allowed for classes with no user-provided constructors, and (in the words of the standard, C++11 8.5.1/2), "each member is copy-initialised from the corresponding initialiser-clause". So no constructor for MyHandle is used, only a copy, move or conversion constructor for each member of class type.

The implicit default constructor, which default-initialises each member, is used for default and value initialisation; but it can't be used for aggregate initialisation since each member can only be initialised once.

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.

Does std::make_shared perform value initialization (GCC and clang disagree)?

Yes.

N3797 20.8.2.2.6

Allocates memory suitable for an object of type T and constructs an
object in that memory via the placement new expression ::new (pv) T(std::forward<Args>(args)...)

So, here will be

::new (pv) int();

And so on by N3797 8.5.1

The initialization that occurs in the forms

T x(a);
T x{a};

as well as in new expressions (5.3.4) is called direct-initialization.

The semantics of initializers are as follows. The destination type is
the type of the object or reference being initialized and the source
type is the type of the initializer expression. If the initializer is
not a single (possibly parenthesized) expression, the source type is
not defined.

— If the initializer is (), the object is value-initialized.

To value-initialize an object of type T means:

— otherwise, the object is zero-initialized.

And both new clang and GCC agree with the standard: Live

std::make_shared not working, but creating the pointer using new works fine

As a template function, std::make_shared tries to find the appropriate constructor for your class given the parameters it has. Since you've given it initializer lists (the stuff in brackets), it is confused about what type those lists are supposed to initialize, and it thus can't find the appropriate constructor. However, when you use the constructor proper, ambiguity is removed, since thanks to the parameters' position the compiler knows what type the lists are supposed to initialize, and it converts them accordingly.

If you still want to use std::make_shared, you'll have to disambiguate the types of the initializer lists by putting them before the list :

this->playerhullbar = std::make_shared<GUImovingbar>(
"right",
Scoords { 50,hully },
globalguitextures[findStringSrdPointerPairVectElement(globalguitextures, "barbackground")].second,
Rect { 0,static_cast<double>(maxplayerhullint),static_cast<double>(maxplayerhullint) },
std:vector<int> { 50,hully,250, hully,2,100 },
Color { 0,255,0 },
bartextvect
);

(or, if you have an old compiler, use the former syntax with parentheses as well : std:vector<int>({ 50,hully,250, hully,2,100 }))

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.

Avoiding extra move in make_unique/make_shared/emplace/etc for structures that use aggregate initialization

Instead of adding a constructor to your type that takes a factory function, instead create a new external factory object with a conversion operator to your type. With C++17, that takes minimal work:

template <class F>
struct factory {
F f;

operator invoke_result_t<F&>() { return f(); }
};

template <class F>
factory(F ) -> factory<F>;

For your earlier example, S doesn't need the constrained constructor anymore. You would instead do:

optional<S> s;
s.emplace( factory{[]{ return foo(); }} ); // or really just factory{foo}

Which prints just ctor and dtor. Since we're not modifying S in any way, we could use this in aggregates as well - like D.



Related Topics



Leave a reply



Submit