14

My default behaviour for any objects in local scopes is to make it const. E.g.:

auto const cake = bake_cake(arguments);

I try to have as little non-functional code as I can as this increases readability (and offers some optimisation opportunities for the compiler). So it is logical to also reflect this in the type system.

However, with move semantics, this creates the problem: what if my cake is hard or impossible to copy and I want to pass it out after I'm done with it? E.g.:

if (tastes_fine(cake)) {
  return serve_dish(cake);
}

As I understand copy elision rules it's not guaranteed that the cake copy will be elided (but I'm not sure on this).

So, I'd have to move cake out:

return serve_dish(std::move(cake)); // this will not work as intended

But that std::move will do nothing useful, as it (correctly) will not cast Cake const& to Cake&&. Even though the lifetime of the object is very near its end. We cannot steal resources from something we promised not to change. But this will weaken const-correctness.

So, how can I have my cake and eat it too?

(i.e. how can I have const-correctness and also benefit from move semantics.)

bitmask
  • 32,434
  • 14
  • 99
  • 159
  • 2
    If you really wanted to, you could write a wrapper class with a non-const instance inside, that exposes a const reference to it and a `move_from` method. In debug builds you could add an assertion to prevent objects from being used after being moved from. But honestly, I would simply drop the `const` and be done with it. – HolyBlackCat May 24 '20 at 15:04
  • The only thing that would come into my mind would be to use PIMPL idiom, and make the `unique_ptr` to impl `mutable`, or move construct the impl, as the element the `unique_ptr` points to does not "inherit" the constness. Both things are kinda janky. – t.niese May 24 '20 at 15:06
  • @HolyBlackCat Yes, I thought about that, but I see three problems with it. First, it doesn't visually tell you what's going on, I still have to write `W cake` instead of `Cake const cake` (this could be mitigated by making the held instance `mutable`). Second, `auto` is out the window. Third, it takes away optimisation opportunities for the compiler because the held instance is no longer `const` (it merely put on a const-looking dress). – bitmask May 24 '20 at 15:07
  • What about `auto cake = bake_cake(...); const auto& cake_r = cake;`? Then, you can use `cake_r` during all the code except of that return statement, where you can employ `std::move` with the desired effect. – Daniel Langr May 24 '20 at 15:19
  • For "First", it is just naming I would say, `Const cake` versus `const Cake cake`. For "second", CTAD might replace `auto`: `Const cake = makeCake();` versus `const auto cake = makeCake();`. – Jarod42 May 24 '20 at 15:19
  • 1
    @DanielLangr: optimization from `const` is also lost. (mutate through const reference is still possible whereas mutate const object is UB). – Jarod42 May 24 '20 at 15:26
  • @bitmask: NRVO is not guarantied (but `std::move` is else used when "possible"), and only apply to `return cake` anyway. not to last usage. – Jarod42 May 24 '20 at 15:29
  • @Jarod42 **Exactly because** it is not guaranteed I want to be able `std::move` it. – bitmask May 24 '20 at 15:31
  • @Jarod42 True, but calling member functions via const-ref will use their const overloads, which may themselves be more optimized. I believe this problem is too generic, we don't see any details of what exactly is done with classes and how they are defined. – Daniel Langr May 24 '20 at 15:33
  • Effectively, this questions asks for a "const until the very last use". – bitmask May 24 '20 at 15:33
  • @DanielLangr The question is intentionally geneeric, as I want a generic solution. This has been bothering me for a while now. – bitmask May 24 '20 at 15:35
  • As limited work around, you might provide `Cake(const Cake&&)` with `mutable` flag to prevent releasing resource (but not compatible with stdandard containers/smart pointers). – Jarod42 May 24 '20 at 15:36
  • The only way I see is to have all members as mutable, and use a move constructor which has `const &&` parameter. But you still lose const-related optimization this way. – geza May 24 '20 at 15:36
  • This is a "the rule-of-five is dead, long live the rule-of-seven" kind of situation. `const&&` has been humming around in my head as well, but I'm not sure this can of joy should be opened. Ever. – bitmask May 24 '20 at 15:39
  • BTW, do you have any evidence that making an object `const` effectively results in more optimized assembly in comparison with non-const object, when only const member functions are called for it? – Daniel Langr May 24 '20 at 15:41
  • C++ doesn't track lifetime, contrary to rust. – Jarod42 May 24 '20 at 15:41
  • @DanielLangr: it could. For example, if some member of cake is initialized with a constant expression, then the compiler can assume that it won't change, even if it is passed to a function by reference. If cake is not const, then it can be changed, even by a function which takes cake as a const reference (because the function can cast away the const). – geza May 24 '20 at 15:45
  • 1
    @DanielLangr: we can save some `load`. in `foo(const int&); const int i = 42; foo(i); return i;` we know we return 42; in `int i = 42; foo(i); return i;` we have to reload `i` which might have changed in `foo`. – Jarod42 May 24 '20 at 15:46
  • @bitmask IIRC, you can legally cast const away and modify the referenced object if it itself is not const. – Daniel Langr May 24 '20 at 15:51
  • `int not_const = 42; [](const int& i){++const_cast(i);}(not_const);` is legal. – Jarod42 May 24 '20 at 15:52
  • @DanielLangr Oh shoot, you are correct, I literally just looked this up the other day. – bitmask May 24 '20 at 15:52
  • **Your code does not respect `const` correctness if your variable can be modified.** Also, if a variable is declared `const`, then a possible optimization would be to move it into read-only memory and assume that it will never change. **So moving from it, would make absolutely no sense at all.** – Phil1970 May 24 '20 at 17:16
  • @Phil1970 **Absolutely**. Maybe the question should have been phrased differently, focussing more on *guaranteeing copy elision* [for const objects] instead of on *move semantics* [for const objects]. Which, as you say, is impossible in the literal sense. – bitmask May 24 '20 at 18:00
  • I would assume that the signature is serve_dish(Cake cake); because a reference wouldn't make sense. However, if Cake is difficult to copy - then according to http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rf-in you should pass it as const-reference. – Hans Olsson Aug 20 '20 at 13:24

5 Answers5

6

I believe it's not possible to move from a const object, at least with a standard move constructor and non-mutable members. However, it is possible to have a const automatic local object and apply copy elision (namely NRVO) for it. In your case, you can rewrite your original function as follows:

Cake helper(arguments)
{
   const auto cake = bake_cake(arguments);
   ...  // original code with const cake
   return cake;  // NRVO 
}

Then, in your original function, you can just call:

return serve_dish(helper(arguments));

Since the object returned by helper is already a non-const rvalue, it may be moved-from (which may be, again, elided, if applicable).

Here is a live-demo that demonstrates this approach. Note that there are no copy/move constructors called in the generated assembly.

Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
  • Interesting. I suppose this also works if you have several `cake`s that you want to pass to `serve_dish`. Unless *these* need to interact in the same scope. – bitmask May 24 '20 at 16:39
  • 1
    @bitmask Yes, that's the drawback of this approach — breaking the single scope into two. – Daniel Langr May 24 '20 at 16:40
  • Uhm, if `helper` was a lambda, then cake-interaction will be possible again (with minor caveats) and the compiler should be able to inline the local lambda. Am I overlooking something? – bitmask May 24 '20 at 16:43
  • @bitmask I believe that's right. (A reader of such code might be quite confused why the lambda is there, so I would suggest to put some explaining comment, such as a link to this question :). – Daniel Langr May 24 '20 at 16:44
  • Thinking about it, I'm not sure how the symbol would be accessible from outside the lambda (a `Cake const*` in the surrounding scope is probably a bad idea). So I think not much will be gained by my suggestion after all. – bitmask May 24 '20 at 16:50
3

It seems to me, that if you want to move, than it will be "const correct" to not declare it const, because you will(!) change it. It's ideological contradiction. You cannot move something and leave in place at the same time. You mean, that object will be const for a part of time, in some scope. In this case, you can declare const reference to it, but it seems to me, that this will complicate the code and will add no safety. Even vice versa, if you accidentally use the const reference to object after std::move() there will be problems, despite it will look like work with const object.

3

Make them movable if you can.

It's time to change your "default behaviour" as it's anachronistic.

If move semantics were built into the language from inception then making automatic variables const would have quickly become established as poor programming practice.

const was never intended to be used for micro-optimisations. Micro-optimisations are best left to the compiler. const exists primarily for member variables and member functions. It's also helped clean up the language a little: e.g. "foo" is a const char[4] type whereas in C it's a char[4] type with the curious understanding that you're not allowed to modify the contents.

Now (since C++11) const for automatic variables can actually be harmful as you observe, the time has come to stop this practice. The same can be said for const parameter by-value types. Your code would be less verbose too.

Personally I prefer immutable objects to const objects.

Bathsheba
  • 231,907
  • 34
  • 361
  • 483
  • This answer is opinion based and I disaggree. Const correctness at function variable scope is extremely helpfull when reasoning about code (and even while programming it) !! – darune Aug 18 '20 at 09:09
  • @darune: Out of interest, what are your thoughts on languages that don't have `const`? (E.g. Java, Python, early C). – Bathsheba Aug 18 '20 at 09:12
  • Well, we are taking about c++ here. Other langs have a lot of other pro's and con's when compared. Eg. I wouldn't normally use c++ for what i use python for for example, being two completely different beasts (even 'python' has immutable but works somewhat different). Java is perhaps easier to compare, but use other 'paradigms' to get sort of the same kind of thing (ie. a getter without setter) and also has the final keyword. – darune Aug 18 '20 at 10:28
  • 1
    I don't think this is opinion based, `const` is good and helps with readability sometimes but when you know the const is actually going to pessimize (in some cases), then its best to just do the simplest thing i.e., drop the const. – Waqar Aug 18 '20 at 10:30
  • @Waqar I agree with that, no reason to pessimize, if it makes your life and code simpler then just drop the 'const' (I do that myself a lot due to productivity) - that being said, for library level code and extensive functions it can help a great deal. – darune Aug 18 '20 at 10:36
  • @darune it does help a lot, and with some frameworks (notably Qt), const by default is the right thing to do and is an optimization as it saves us from "unintentional container detach" cases – Waqar Aug 18 '20 at 10:39
  • What do you mean by immutable objects (opposed to const)? – geza Aug 18 '20 at 11:54
  • @geza: An object is immutable if it cannot be changed once it's created. (Aside from other things like helping concurrency and program stability, you also don't need to make them `const`, and so they are movable.) – Bathsheba Aug 18 '20 at 12:26
  • Do you mean a class which intentionally doesn't have functions which mutate the object? – geza Aug 18 '20 at 14:34
  • @geza: That's the one! – Bathsheba Aug 18 '20 at 15:10
3

You should indeed continue to make your variables as that is good practice (called ) and it also helps when reasoning about code - even while creating it. A object cannot be moved from - this is a good thing - if you move from an object you are almost always modifying it to a large degree or at least that is implied (since basically a move implies stealing the resources owned by the original object) !

From the core guidelines:

You can’t have a race condition on a constant. It is easier to reason about a program when many of the objects cannot change their values. Interfaces that promises “no change” of objects passed as arguments greatly increase readability.

and in particular this guideline :

Con.4: Use const to define objects with values that do not change after construction


Moving on to the next, main part of the question:

Is there a solution that does not exploit NRVO?

If by NRVO you take to include guaranteed copy elision, then not really, or yes and no at the same. This is somewhat complicated. Trying to move the return value out of a return by value function doesn't necessarily do what you think or want it to. Also, a "no copy" is always better than a move performance-wise. Therefore, instead you should try to let the compiler do it's magic and rely in particular on guaranteed copy elision (since you use ). If you have what I would call a complex scenario where elision is not possible: you can then use a move combined with guaranteed copy elision/NRVO, so as to avoid a full copy.

So the answer to that question is something like: if you object is already declared as const, then you can almost always rely on copy-elision/return by value directly, so use that. Otherwise you have some other scenario and then use discretion as to the best approach - in rare cases a move could be in order(meaning it's combined with copy-elision).

Example of 'complex' scenario:

std::string f() {
  std::string res("res");
  return res.insert(0, "more: ");//'complex scenario': a reference gets returned here will usually mean a copy is invoked here.
}

Superior way to 'fix' is to use copy-elision i.e.:

return res;//just return res as we already had that thus avoiding copy altogether - it's possible that we can't use this solution for more *hairy/complex* scenarios.

Inferior way to 'fix' in this example would be;

return std::move(res.insert(0, "more: "));
Bathsheba
  • 231,907
  • 34
  • 361
  • 483
darune
  • 10,480
  • 2
  • 24
  • 62
2

A limited workaround would be const move constructor:

class Cake
{
public:
    Cake(/**/) : resource(acquire_resource()) {}
    ~Cake() { if (owning) release_resource(resource); }

    Cake(const Cake& rhs) : resource(rhs.owning ? copy_resource(rhs.resource) : nullptr) {}
    // Cake(Cake&& rhs) // not needed, but same as const version should be ok.
    Cake(const Cake&& rhs) : resource(rhs.resource) { rhs.owning = false; }

    Cake& operator=(const Cake& rhs) {
        if (this == &rhs) return *this;
        if (owning) release_resource(resource);
        resource = rhs.owning ? copy_resource(rhs.resource) : nullptr;
        owning = rhs.owning;
    }
    // Cake& operator=(Cake&& rhs) // not needed, but same as const version should be ok.
    Cake& operator=(const Cake&& rhs) {
        if (this == &rhs) return *this;
        if (owning) release_resource(resource);
        resource = rhs.resource;
        owning = rhs.owning;
        rhs.owning = false;
    }
    // ...

private:
    Resource* resource = nullptr;
    // ...
    mutable bool owning = true;
};
  • Require extra mutable member.
  • not compatible with std containers which will do copy instead of move (providing non const version will leverage copy in non const usage)
  • usage after move should be considered (we should be in valid state, normally). Either provide owning getter, or "protect" appropriate methods with owning check.

I would personally just drop the const when move is used.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • 1
    And probably bring back problems associated with `auto_ptr`... like not working as expected in some corner cases. Really a bad idea to write code that goes against the intended way of doing things. Not sure if it was a good idea to give an answer as OP might be tempted to write discutable code! – Phil1970 May 24 '20 at 17:20
  • @Phil1970 Never mind that, I'm already always tempted to do discutable and disputable things ;) – bitmask May 24 '20 at 17:57