When is C++23 auto(x) useful?
Asked Answered
J

1

22

I'm looking for a simple example when C++ 23 auto(x) could be useful.

This is what I have so far:

struct  A {
    A() = default;
    explicit A(const A&) {} // copy constructor
};
struct B {
    A child;
};

template<class T>
void printChild(T t) {
}

template<class T>
void printParent(T t) {
    // printChild(t.child); // Error - copy ctr is explicit
    printChild(A(t.child)); // if we knew the type
    printChild(auto(t.child)); // in C++23
}


   
int main() {
    B b;
    printParent(b);
}

Now I'm looking for a simple example without an explicit constructor, and perhaps another one where the decay_copy benefit is shown. The examples I've found online so far have not been clear to me.

Jurdi answered 13/6 at 6:4 Comment(6)
I would advise against using auto at all here but that is my personal preference.Ricotta
Why not just read the original proposal? If there's anything unclear after you have read that please clarify.Pairoar
this is after I've read. If it were clear to me, I wouldn't ask the question. Normally I refrain from asking questions on things that are clear to me.Jurdi
You said in a comment that you're unclear about the erase example. You cannot take the value to be erased by reference because the reference will be invalidated after erasure. That particular example has nothing to do with constructors.Pairoar
thanks for clarifying. but I see erase receives by const&. I thought const references essentially behave like copies, with the value staying on the called stack frame while the local variable accessing it is in scopeJurdi
I guess that's where your confusion comes from then. A const reference does not behave like a copy.Pairoar
T
28

auto(...) has the benefit that it always clearly communicates that a copy is needed and intended. This is one of the motivations for the original proposal, P0849R8: auto(x): decay-copy in the language

While you could write

// assuming non-explicit copy constructor
auto copy = t.child;
printChild(copy);

... it's not obvious to the reader that the extra variable is needed (or not). By comparison, printChild(auto(t.child)); is expressing the intent to copy very clearly, and it works even if you don't know the type of copy or if the type is very lengthy and annoying to spell out.

Of course, since printChild accepts any T by value, you could just write printChild(t.child) and let the copy take place implicitly. However, in generic code, you typically work with forwarding references or other kinds of references, not values. You don't want to pass things by value if you don't know whether they're small types.

A motivating example comes from the proposal itself (slightly adapted):

void pop_front_alike(Container auto& x) {
    std::erase(x, auto(x.front()));
}

Note: the copy of x.front() is needed here because erasing vector contents would invalidate the reference obtained from x.front() and passed to std::erase.

Outside of templates, you often should pass stuff by rvalue reference as well, as recommended by CppCoreGuidelines F.18: For “will-move-from” parameters, pass by X&& and std::move the parameter:

// CppCoreGuidelines recommends passing vector by rvalue ref here.
void sink(std::vector<int>&& v) {
    store_somewhere(std::move(v));
}

struct S {
    std::vector<int> numbers;
    void foo() {
        // We need to copy if we don't want to forfeit our own numbers:
        
        sink(numbers);                   // error: rvalue reference cannot bind to lvalue
        sink(std::vector<int>(numbers)); // OK but annoying
        sink(auto(numbers));             // :)
    }
};

Last but not least, you can simply look at the C++20 standard. There are 43 occurrences of decay-copy in the document, and any use of decay-copy can usually be replaced with auto(x). To name some examples,

  • std::ranges::data(t) may expand to decay-copy(t.data()), and
  • the std::thread constructor applies decay-copy to each argument.
Treenware answered 13/6 at 6:23 Comment(6)
Great, but then here's what I don't get. If there is a non explicit copy constructor - why would I need to create a manual copy? It would just get copied naturally.Jurdi
Technically yes, but it's poor style. If you follow CppCoreGuidelines and/or write a lot of templates, you almost never pass expensive to-copy-types by value. The copies will have to take place explicitly. It's also not really a good thing that you can write innocent-looking code like store(numbers) and have it perform a copy of a billion elements in a vector implicitly. In the sink example, you take an rvalue reference, so that's not possibly to write anyway.Treenware
Why couldn't it be implemented as a library function then? Something like copy(x).Interlace
@PasserBy see open-std.org/jtc1/sc22/wg21/docs/papers/2021/… Among other reasons, copy(x.front()) would at least move x.front() even if it was a prvalue, but auto(x.front()) is a no-op in that case. copy(...) also fails when copying something with a non-public move/copy constructor, where auto(...) may succeed.Treenware
I see. x.front() being a prvalue is very unlikely though. The more I think about it, the less I'm sure there is a compelling case where it's unknown whether an expression is a prvalue, even in a template.Interlace
Yes, it's not very common. The C++20 standard has a few places where decay-copy is almost always applied to prvalues, like decay-copy(t.begin()) for std::ranges::begin(t), and those can avoid an unnecessary copy/move with auto(t.begin()) now. You could run into a case where you really need to cover prvalues when doing something like the std::thread constructor but accepting a pack of callable objects, so each call operator could yield any value category but you still want to store everything as an object with minimal cost.Treenware

© 2022 - 2024 — McMap. All rights reserved.