You should indeed continue to make your variables const as that is good practice (called const-correctness) and it also helps when reasoning about code - even while creating it. A const 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 c++17). 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: "));