Choosing the way of initialization in make_<something>

266 views
Skip to first unread message

d.n.i...@gmail.com

unread,
Feb 26, 2017, 6:03:03 AM2/26/17
to ISO C++ Standard - Future Proposals
The essense of the problem may be shown by the std::make_from_tuple function.

Case 1: trivial type

struct trivial
{
   
int x;
   
double y;
};

make_from_tuple
<trivial>(make_tuple(1, 3.14)); // Compilation error.

The example shows that this case is simply not covered by the current version of the standard library. Trivial type can't be created from a tuple by std::make_from_tuple function.

Case 2: the existence of several variants of the constructor

make_from_tuple<vector<size_t>>(make_tuple(5, 1));
// Which vector should be created?
// [1, 1, 1, 1, 1] or [5, 1]?

In the current edition we'll get "five ones". Ant there is no possibility to get "five and one".

Proposed solution

The main idea is tag dispatching.
We create two structs, each of which will signal about the need to call either round or curly brackets (names are rough):

template <typename T>
struct parens_t {};

template <typename T>
constexpr auto parens = parens_t<T>{};

template <typename T>
struct braces_t {};

template <typename T>
constexpr auto braces = braces_t<T>{};

Usage:

auto a = make_unique(parens<A>, 1, 3); // A(1, 3)
auto b = make_unique(braces<A>, 1, 3); // A{1, 3}

auto c = make_from_tuple(parens<A>, make_tuple(100, 3.14)); // A(100, 3.14)
auto d = make_from_tuple(braces<A>, make_tuple(100, 3.14)); // A{100, 3.14}
// Finally we can call the curly brackets. Yay!

Also note that when using this tag, the object type is inferred from the tag, that is, it is not necessary to specify it explicitly:

// Then:
auto a = make_shared<very_very_very_long_type_name>(very_very_very_long_type_name{1, 3});

// Now:
auto b = make_shared(braces<very_very_very_long_type_name>, 1, 3);

The old overloads, of course, remain. The difference is that now we can explicitly choose the way of initialization.
Similarly it would work with functions that accept a set of parameters to pass to the constructor, for example, emplace_back.

Example to follow

This approach is not new for the standard library. A similar scheme is used with the tag std::in_place_type, for example, in the constructor of the std::variant class. This tag indicates that we need to call the constructor of the emplaced class. But in our case we go a little further. We provide the opportunity not just to signal that we need to call the constructor, but to tell which exactly constructor it's supposed to be.

Summary

The main point is to give user an opportunity to choose between the direct-initialization and the direct-list-initialization explicitly. There are many cases when it matters.

Given solution:
  1. Simplifies programmer's life and enhances the functionality of the standard library.
  2. Maintains full backward compatibility. Nothing is broken.
  3. Is quite general. It works with make_sharedmake_uniquemake_anymake_from_tuple, emplace and more.
  4. Seamlessly fits into the standard library and is pretty convenient to use.

Vicente J. Botet Escriba

unread,
Feb 26, 2017, 10:00:26 AM2/26/17
to std-pr...@isocpp.org
I like the idea. Instead of parens/braces we would need something better.

What about considering other kind of constructions as e.g. default initialization, no parens, no braces?

Vicente

Nicol Bolas

unread,
Feb 26, 2017, 10:03:15 AM2/26/17
to ISO C++ Standard - Future Proposals, d.n.i...@gmail.com
On Sunday, February 26, 2017 at 6:03:03 AM UTC-5, d.n.i...@gmail.com wrote:
The essense of the problem may be shown by the std::make_from_tuple function.

Case 1: trivial type

struct trivial
{
   
int x;
   
double y;
};

make_from_tuple
<trivial>(make_tuple(1, 3.14)); // Compilation error.

The example shows that this case is simply not covered by the current version of the standard library. Trivial type can't be created from a tuple by std::make_from_tuple function.

See LWG 2089.
 

Case 2: the existence of several variants of the constructor

make_from_tuple<vector<size_t>>(make_tuple(5, 1));
// Which vector should be created?
// [1, 1, 1, 1, 1] or [5, 1]?

In the current edition we'll get "five ones". Ant there is no possibility to get "five and one".

Why would you want to? If you wanted "five and one", you would pass an `initializer_list`. Yes, it's longer, but so be it.

The principle downside of this is that it enhances only "the functionality of the standard library". Which means that if someone writes their own types with such indirect initialization capabilities, they would have to manually provide such functionality. Every time. And existing functions would have to be updated as well.

If you're going to try to establish such a convention through the library, then you need lower-level support. I would strongly encourage you to add a `std::initialize<T>` function to the library which would do the low-level dispatching work. This probably won't get used by novices, but at least experts will be able to easily write functions that adhere to the standard library's conventions on this sort of thing.

Also, I advise you to remove the `T` to be constructed from the tag's typename. The reason being that there are several indirect initialization cases where `T` is well-defined. `vector<int>::emplace` cannot emplace a `float` or a `SomeType`; it will only initialize an `int`. So why do we have to say it again? For the `make/allocate_` style functions, you're going to have to specify the type anyway. So why is it better on the tag than the function call? The only place where putting it on the tag would be a win would be with `variant`/`any`'s `in_place_type` tagged initialization.

template<typename T, typename ...Args>
T initialize
(Args &&...args)
{
 
return T(std::forward<Args>(args)...);
}

template<typename T, typename ...Args>
T initialize
(std::construct_t, Args &&...args)
{
 
return T(std::forward<Args>(args)...);
}

template<typename T, typename ...Args>
T initialize
(std::list_init_t, Args &&...args)
{
 
return T{std::forward<Args>(args)...};
}

I changed the names to match standard terminology. But you get the general idea.

In any case, I maintain a list of various fixes for this initialization problem. This one is already on that list.

Nicol Bolas

unread,
Feb 26, 2017, 10:24:40 AM2/26/17
to ISO C++ Standard - Future Proposals
On Sunday, February 26, 2017 at 10:00:26 AM UTC-5, Vicente J. Botet Escriba wrote:
I like the idea. Instead of parens/braces we would need something better.

What about considering other kind of constructions as e.g. default initialization, no parens, no braces?

Default initialization has to be a language feature. There's really no way for a function to return a default initialized object while simultaneously taking advantage of guaranteed elision. Oh, you can do `T t; return t`, but that requires that `T` be copy/moveable.

To do this properly, especially if you want to expose this for other users to use, it has to be a language feature of some kind, something that would allow you to return a default initialized temporary.

Vicente J. Botet Escriba

unread,
Feb 26, 2017, 11:27:08 AM2/26/17
to std-pr...@isocpp.org
I can want to create e.g. a unique_ptr<POD> with the POD uninitialized, isn't it?

auto a = make_unique(uninitialized<POD>); // as if new POD

or  zero initialized

auto a = make_unique(zero_initialized<POD>); // as if new POD{}


Am I missing surely something important.

Vicente

Nicol Bolas

unread,
Feb 26, 2017, 11:38:32 AM2/26/17
to ISO C++ Standard - Future Proposals

First, that's not zero initialization; that's value initialization. Yes, value initialization can cause zero initialization, but the syntax itself invokes value initialization.

More importantly, how does that get implemented? How does it get implemented for each form of indirect initialization? Because it's going to have to be different if its a library feature, since container `emplace` logic forwards all construction to `allocator`/`traits` classes.

d.n.i...@gmail.com

unread,
Feb 27, 2017, 1:04:06 PM2/27/17
to ISO C++ Standard - Future Proposals, d.n.i...@gmail.com
The "initialize" thing is good.

But there is a case it does not cover:

struct A
{
    A
(A &&) = delete;

    A
(std::initializer_list<int>) {}

    A
(int, int) {}
};

int main()
{
   
auto a = make_unique<A>(A{17, 3}); // error: call to deleted constructor of 'A'
}

Is there a way to direct-list-initialize such a non-movable object?

воскресенье, 26 февраля 2017 г., 18:03:15 UTC+3 пользователь Nicol Bolas написал:

Nicol Bolas

unread,
Feb 27, 2017, 1:41:03 PM2/27/17
to ISO C++ Standard - Future Proposals, d.n.i...@gmail.com
On Monday, February 27, 2017 at 1:04:06 PM UTC-5, d.n.i...@gmail.com wrote:
The "initialize" thing is good.

But there is a case it does not cover:

struct A
{
    A
(A &&) = delete;

    A
(std::initializer_list<int>) {}

    A
(int, int) {}
};

int main()
{
   
auto a = make_unique<A>(A{17, 3}); // error: call to deleted constructor of 'A'
}

Is there a way to direct-list-initialize such a non-movable object?

Yes. That's what we're talking about. If `make_unique` uses `std::initialize` as I have defined it, you would do this:

make_unique<A>(std::list_init, 17, 3);

You would implement `make_unique` like this:

template<typename T, typename ...Args)
unique_ptr
<T> make_unique(Args &&...args)
{
 
return unique_ptr<T>(new auto(std::initialize<T>(std::forward<Args>(args)...)));
}

Guaranteed elision allows this to work no matter what constructors `T` does or does not have. Well, so long as it can be constructed with the given arguments.
Reply all
Reply to author
Forward
0 new messages