Why does implementation of make_tuple not return via brace initialisation?
Asked Answered
E

1

2

To understand the question please read this answer first.

I checked different historic make_tuple implementations (including clang versions of 2012). Before C++17 I would have expected them to return {list of values ... } but they all construct the tuple before returning it. They are all along the lines of the very simplified current cppreference example:

template <class... Types>
auto make_tuple(Types&&... args)
{
    return std::tuple<special_decay_t<Types>...>(std::forward<Types>(args)...);
}

Its not wrong but the point of returning brace initialization is to construct the returned object directly. Before C++17 there was no guaranteed copy elision which removes the temporaries even conceptually. But even with C++17 I would not necessarily expect the curly braces to disappear in this example.

Why no curly braces here in any of the C++11/14 implementations? In other words, why not

 template <class... Types>
    std::tuple<special_decay_t<Types>...> make_tuple(Types&&... args)
    {
        return {std::forward<Types>(args)...};
    }
Entomologize answered 31/1, 2018 at 1:25 Comment(10)
You can't do that with return type auto.Custodian
@aschepler, yes, I know, but why is that a problem?Entomologize
A braced-init-list has no type, so the return type cannot be deducedSixtasixteen
@Praetorian, yes, I know, the return type cannot be auto, why must it be auto?Entomologize
Because a braced initializer list alone is not assumed to be an initializer for a std::tuple. Could you make a rule that says that a braced initialization list for an auto results in a std::tuple? You could, but there is no such rule in C++.Vend
Oh, you're asking why auto isn't replaced with std::tuple<special_decay_t<Types>...> and then a braced-init-list used in the return statement. You should edit the question to clarify that.Sixtasixteen
@Praetorian, I just did, but this is why I started of with please read . . . which should have made it clear.Entomologize
@Praetorian, "I did, and it wasn't". I just do not understand that comment.Entomologize
I read the question you linked to, and it still wasn't clear to me what you were askingSixtasixteen
I think your writing isn't emphasizing the combinations enough since it doesn't establish this idea to your readers in early paragraphs. The second paragraph should've established the mention of the combination of "auto and return tuple..." and the combination of "tuple and return {...}", and subsequent paragraphs could refer to the combinations.Feverish
P
6

The advantage you're talking about simply doesn't apply in this case. Yes, you can construct a non-movable type this way:

struct Nonmovable {
    Nonmovable(int );
    Nonmovable(Nonmovable&& ) = delete;
};

Nonmovable foo() { return {42}; } // ok

But in the context of make_tuple, all the elements have to be movable or copyable anyway, because you're going to be moving or copying them into the actual tuple that you're constructing:

std::tuple<Nonmovable> make_tuple(Nonmovable&& m) {
    return {std::move(m)}; // still an error
}

So there's not really an advantage to to:

template <class... Types>
std::tuple<special_decay_t<Types>...> make_tuple(Types&&... args)
{
    return {std::forward<Types>(args)...};
}

over

template <class... Types>
auto make_tuple(Types&&... args)
{
    return std::tuple<special_decay_t<Types>...>(std::forward<Types>(args)...);
}

in that sense.

Yes, by the language of the standard, one of these implies constructing directly into the return object and the other involves a temporary. But in practice, every compiler will optimize this away. The one case where it couldn't we already know doesn't apply - our tuple must be movable.

There is at least one weird downside to using {...} here, which is that on the off-chance a type has an explicit move or copy constructor, returning a braced-init-list doesn't work.


More importantly, as T.C. points out, until Improving pair and tuple was adopted, the constructor for std::tuple was always explicit.

// in C++11
explicit tuple( const Types&... args );

// in C++14
explicit constexpr tuple( const Types&... args );

// in C++17, onwards
/*EXPLICIT*/ constexpr tuple( const Types&... args );

Which made an implementation returning a braced-init-list impossible. So every library would have already had a make_tuple() implementation before C++17, which could not have used a braced-init-list, and there's no benefit to changing it - so we are where we are today.

Psychokinesis answered 31/1, 2018 at 1:47 Comment(2)
Don't forget that that constructor is fully explicit until the "improve pair and tuple" paper.Sylvia
@Sylvia I did forget!Psychokinesis

© 2022 - 2024 — McMap. All rights reserved.