Search code examples
c++autoc++23

When is C++23 auto(x) useful?


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.


Solution

  • 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.