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 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.
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
What Is Double Evaluation and Why Should It Be Avoided
C++: How to Check Type of Files Without Extension
Casting to Void* and Back to Original_Data_Type*
Stopping an Infinite Loop in C++ When Key Is Pressed
Setting Extra Bits in a Bool Makes It True and False at the Same Time
Nat-Traversal Implementation for P2P Connection
Volatile Struct = Struct Not Possible, Why
Using Crypto++ Static Library in a Qt Project
What Is the Fastest Way to Compute Large Power of 2 Modulo a Number
Getting Cannot Allocate Memory Error
How to Start Process on Linux Os in C, C++
Why Double Can Store Bigger Numbers Than Unsigned Long Long
C++11 Member Initialization List Ambiguity
C++: How to Catch Mouse Clicks Wherever They Happen
What Is The Reason for Having Unreserved Identifiers as Built-In Macros in Gcc
Forcing a Constant Expression to Be Evaluated During Compile-Time