0

What is the feature that allows me use auto for non-copyable (and non-movable) types in C++17 and didn't for C++14?

Consider the following code:

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

int main(){
    auto   a1 = A{}; // ok in C++17, not ok in C++14
    auto&& a2 = A{}; // ok in C++17, ok in C++14
}

It turns out that this was invalid code in C++14 but it is valid in C++17. The behavior is consistent in clang and gcc: https://godbolt.org/z/af8mEc

The reason I ask is because until recently I was making my classes (that represent references) non-copyable, among other things to disallow use of auto, but unfortunately it turns out that now the technique doesn't work in C++17.

(In other words, I think the behavior in C++14 was conceptually right.)

Why is auto a1 = A{}; valid for a non-copyable class in C++17? Is some kind of new RVO case?

I think auto is semantically broken for several arguable reasons but at least in C++14 I could prevent the use of auto (but allowed the use of auto&&).

Is there another way at all of preventing the use of auto a = A{}; for a particular class in C++17, or not anymore?

Note: I asked this question some time ago Is there a way to disable auto declaration for non regular types? and found that the solution back then was to disable the copy and move constructors in C++14, this made both conceptual and syntactic sense however this is not the case anymore in C++17.

alfC
  • 14,261
  • 4
  • 67
  • 118
  • 1
    [Mandatory copy elision](https://en.cppreference.com/w/cpp/language/copy_elision#Mandatory_elision_of_copy.2Fmove_operations) would be my guess. In C++17, `auto a1 = A{};` does **not** involve a copy. It's equivalent to `A a1{};`. In C++14, it most likely does not involve a copy either, but a copy constructor is nevertheless required to exist and be callable, even if it's not actually called in the end. – Igor Tandetnik Oct 20 '19 at 03:31
  • 1
    Why do you want to prevent `auto a = A{};`? What do you feel is a problem with it? – Igor Tandetnik Oct 20 '19 at 03:37
  • 2
    `auto` has nothing at all to do with why that code works or doesn't work. `A a = A{};` is just as invalid in C++14 as the `auto` version, and it is just as valid in C++17 as the `auto` version. – Nicol Bolas Oct 20 '19 at 04:00
  • @IgorTandetnik Because the convention I used in C++14 was that if `A` is a reference-lilke type I would disallow the syntax `auto a = A{};` (or more illustrative `auto a = subrange{container, 10, 20}`) and force the syntax `auto&& a = A{}` ( or `auto&& a = subrange{container, 10, 20}`). – alfC Oct 20 '19 at 04:00
  • @NicolBolas, yes, I wanted (and managed) to disallow both and above all the `auto` case. – alfC Oct 20 '19 at 04:01
  • 1
    @alfC: My point is that it has nothing to do with `auto`. There's no way to disallow *just* the `auto` case. So your question is confused. – Nicol Bolas Oct 20 '19 at 04:02
  • @alfC: "*force the syntax auto&& a = A{}*" Wouldn't that just force the syntax `A a = {};` or `A a{};`? I see no reason why a user would prefer `auto&&`, outside of people who are committed to the "almost always `auto`" dogma. – Nicol Bolas Oct 20 '19 at 04:33
  • @NicolBolas to avoid giving the impression that a value object is obtained when generating a reference-like type or in particular an expression template. See here https://quuxplusone.github.io/blog/2018/07/12/operator-auto/. In the links in this question there are discussions on why auto should mean different things depending on the RHS. What I did was to, at least, disable some (the worst) cases. – alfC Oct 20 '19 at 04:58
  • 1
    @alfC: You keep missing my point. You didn't disable `auto`; you disabled *copying*. That's like swatting a fly with a sledgehammer; sure, the fly died, but there's a giant hole in the wall now. – Nicol Bolas Oct 20 '19 at 05:00
  • @NicolBolas, I understand your point. Mine was that by preventing naked `auto` goes nicely together with non-copiyable classes and that was reflected in C++14, but not anymore in C++17. – alfC Oct 21 '19 at 18:41
  • 1
    @alfC: If you understand my point, why is it that the rest of your comment ignored it? One of my points was that your types are not *logically* immobile. The logical behavior of copying a "reference type" is well understood and is conceptually valid. You're only making them immobile because you were exploiting a side effect of making them immobile. So I fail to see how these two "go nicely together" when all of the types you're talking about shouldn't be immobile. – Nicol Bolas Oct 21 '19 at 19:53
  • I understood your point and moved on to say that preventing `auto` was a desirable side-effect, which is all that mattered to me. I called it "disable `auto`" (and not disable `A a =`) because in my library the actual type is sort of hidden, so the only way to refer to the type is through using `auto` (actually `auto&&`). And since my types are references (or try to emulate references) they are in some sense immobile (of course they can be copied into value types but not into the same reference type). The are inmobile in the sense that, like true-references, they are logically un-rebindable. – alfC Jan 24 '20 at 04:01

1 Answers1

1

In C++17, if you give a user a prvalue of some type, the user can always use it to initialize a variable of that type.

Essentially, the definition of a prvalue has changed. In C++14, it was an object; specifically, a temporary object. Therefore, a statement like A a = prvalue_of_type_A; means to move the temporary object into a. The move could be elided, but it is logically a move and therefore A must support move construction.

In C++17, a prvalue is nothing more than an initializer for an object. Which object it initializes depends on how it gets used. Using a prvalue to initialize a variable of the prvalue's type means that you initialize that object. There is no copy or move. You can look at it from the perspective that the prvalue is a nameless object, and A a = prvalue_of_A; is merely giving a name to that object, not creating a new object.

And no, you can't get around it. So long as you are dealing with a genuine prvalue of some type, auto a = prvalue; will always deduce the type of the prvalue and directly initialize the object a, with no copy/move.


In other words, I think the behavior in C++14 was conceptually right.

Well, let's investigate that.

This investigation begins and ends with the full realization that you did not prevent auto a = A{}; from working. You prevented copy/move construction, which has the side-effect of preventing that syntax. This is why guaranteed elision made your "fix" meaningless.

By denying a type the ability to be copy/move constructed, you did indeed prevent this syntax. But there was a whole lot of collateral damage incurred along the way.

The post you linked to gave the rationale for wanting to disable this syntax as the type is non-Regular, and this makes the use of auto "tricky"... for some reason. Ignoring whether those unstated reasons are valid, your solution didn't just remove the "tricky" syntax. It prevented you from doing basic things to the object.

Consider the ramifications of applying this to string_view. You've got this view type, and you want to modify it through a multi-step process. But you want to keep the original view around unmodified. So naturally, you copy the... oh wait, string_view is non-Regular, so you made copying it illegal just to prevent string_view sv = prvalue;. Oops. So now I have to go back to my original source of the string_view to get another one. Assuming I have access to the source, that I wasn't just passed it as a const& parameter.

Essentially, you're saying that the best way to swat a fly is to use a sledgehammer. No matter how annoying the fly is, the wall behind it was probably more important.

So, is guaranteed elision "conceptually right"? Well, one of the main justifications of guaranteed elision was to allow functions that return prvalues to work even for types that are immobile. This is actually quite important. Before C++17, if you wanted to give your class factory functions, but the class itself should logically be immobile, you were out of luck. You could pick one or the other, but not both.

Let's take your subrange example. In C++14, if I wanted to write a function that simply provided defaults for some of the constructor's parameters, or used a specific container, or whatever, that wasn't possible. I couldn't even write a container that had a function which returned a subrange, since I would have to construct one, which (unless I used a raw braced-init-list, which isn't always possible) would provoke a copy/move.

Guaranteed elision fixes all of these problems. You can make immobile classes with private constructors which can only be created through factories. You can make functions that build immobile objects through public interfaces. And so forth.

The C++17 behavior makes your immobile non-Regular types more useful and capable. Is that not "conceptually right"?

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 1
    Using `clone` type methods to duplicate a non-regular type means "go back to the source" is no longer needed. Look at vector of bool and how `auto x = vec[7]` generic code fails to be generic. The problem of `auto` the OP is addressing is well known. – Yakk - Adam Nevraumont Oct 20 '19 at 10:28