Should I make my local variables const or movable?
Asked Answered
O

5

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

Orvah answered 24/5, 2020 at 14:46 Comment(24)
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.Requirement
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.Cynicism
@Requirement 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> 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).Orvah
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.Podagra
For "First", it is just naming I would say, Const<Cake> cake versus const Cake cake. For "second", CTAD might replace auto: Const cake = makeCake(); versus const auto cake = makeCake();.Sapid
@DanielLangr: optimization from const is also lost. (mutate through const reference is still possible whereas mutate const object is UB).Sapid
@bitmask: NRVO is not guarantied (but std::move is else used when "possible"), and only apply to return cake anyway. not to last usage.Sapid
@Sapid Exactly because it is not guaranteed I want to be able std::move it.Orvah
@Sapid 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.Podagra
Effectively, this questions asks for a "const until the very last use".Orvah
@DanielLangr The question is intentionally geneeric, as I want a generic solution. This has been bothering me for a while now.Orvah
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).Sapid
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.Decimate
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.Orvah
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?Podagra
C++ doesn't track lifetime, contrary to rust.Sapid
@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).Decimate
@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.Sapid
@Orvah IIRC, you can legally cast const away and modify the referenced object if it itself is not const.Podagra
int not_const = 42; [](const int& i){++const_cast<int&>(i);}(not_const); is legal.Sapid
@DanielLangr Oh shoot, you are correct, I literally just looked this up the other day.Orvah
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.Smackdab
@Smackdab 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.Orvah
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 isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rf-in you should pass it as const-reference.Flavoring
P
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.

Podagra answered 24/5, 2020 at 16:35 Comment(5)
Interesting. I suppose this also works if you have several cakes that you want to pass to serve_dish. Unless these need to interact in the same scope.Orvah
@Orvah Yes, that's the drawback of this approach — breaking the single scope into two.Podagra
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?Orvah
@Orvah 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 :).Podagra
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.Orvah
G
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.

Girlie answered 24/5, 2020 at 15:54 Comment(0)
F
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.

Fayefayette answered 17/8, 2020 at 13:59 Comment(10)
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) !!Flaunch
@darune: Out of interest, what are your thoughts on languages that don't have const? (E.g. Java, Python, early C).Fayefayette
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.Flaunch
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.Infernal
@Infernal 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.Flaunch
@Flaunch 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" casesInfernal
What do you mean by immutable objects (opposed to const)?Decimate
@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.)Fayefayette
Do you mean a class which intentionally doesn't have functions which mutate the object?Decimate
@geza: That's the one!Fayefayette
F
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: "));
Flaunch answered 18/8, 2020 at 9:57 Comment(1)
You know my feelings already, but this answer is still useful all the same; +1.Fayefayette
S
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.

Sapid answered 24/5, 2020 at 16:28 Comment(2)
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!Smackdab
@Smackdab Never mind that, I'm already always tempted to do discutable and disputable things ;)Orvah

© 2022 - 2024 — McMap. All rights reserved.