7

I realized that the common-knowledge that "you cannot move a const object" is not entirely true. You can, if you declare the move ctor as

X(const X&&);

Full example below:

#include <iostream>

struct X
{
    X() = default;
    X(const X&&) {std::cout << "const move\n";}
};

int main()
{
    const X x{};
    X y{std::move(x)};
}

Live on Coliru

Question: is there any reason why one would want such a thing? Any useful/practical scenario?

vsoftco
  • 55,410
  • 12
  • 139
  • 252
  • 1
    But can a "move" constructor declared as `X(const X&&)` actually _move_ anything? – Emil Laine Apr 18 '17 at 19:08
  • 1
    @tuple_cat Well, depends on what you mean by "move". A move is just a rvalue conversion, it's up to you what you do with it. In a sense, that's why I asked the question, I cannot see any use for it. There is one exception, but it's slightly different: `f(const X&&) = delete;` to disable rvalue binding, see https://github.com/vsoftco/snippets/blob/master/disabling_rvalue_binding.cpp for a full commented code why this is the case. – vsoftco Apr 18 '17 at 19:09
  • 2
    IMO moving means actually moving something, not just converting to rvalue. Though I don't know what the standard specifies as "moving". – Emil Laine Apr 18 '17 at 19:10
  • @tuple_cat I think the standard just specify what `std::move` does, i.e. convert to `X&&` – vsoftco Apr 18 '17 at 19:12
  • 2
    By itself, an rvalue reference can certainly point at a const object in general. But in the specific case of a move constructor or move assignment operator, it should not be passed a reference to a const object, since the constructor/operator is expected to modify the object (by taking ownership of data and then altering the object into a valid default state). Unless there is no data to move around, in which case there is no need to have a move constructor/operator in the first place. – Remy Lebeau Apr 18 '17 at 19:13
  • `std::move` is a cast but an actual mover operation (move constructor call/move assignment operator) should actually move things from one object into the other. – NathanOliver Apr 18 '17 at 19:14
  • But does const mean anything, if the object has no state? – Kenny Ostrom Apr 18 '17 at 19:14
  • 6
    """I realized that the common-knowledge that "you cannot move a const object" is just false""" That common knowledge is using common parlance, which means "move" is intended to convey *actually moving something*. You can't take something, change the definitions of some words, and then claim the original is false. – GManNickG Apr 18 '17 at 19:15
  • 2
    @GManNickG Fair enough, but it's a semantics issue: many people believe that `std::move` DOES something. It doesn't, it's just a conversion. It's up to the move ctor to decide what to do with the rvalue. – vsoftco Apr 18 '17 at 19:17
  • @tuple_cat I actually knew that, and mentioned in one of the comments. This is slightly different, as it directly refers to the move ctor, not to another function. I won't reopen the question, if someone thinks it should please do. – vsoftco Apr 18 '17 at 19:20
  • 1
    If you want distinguish const ref from const temporary... But I don't see practical usage. – Jarod42 Apr 18 '17 at 20:13

3 Answers3

2

Your example doesn't move anything. Yes, you wrote std::move to get an rvalue and you invoked a move constructor, but nothing actually ends up getting moved. And it can't, because the object is const.

Unless the members you were interested in were marked mutable, you would not be able to do any "moving". So, there is no useful or even possible scenario.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • You're absolutely right, however I'm wondering whether there's any real use case. One thing that came to my mind is in lambda capturing by value: the object will be captured as `const`, but of course can be casted back to non-`const` and moved from (provided the initial def. is non-`const` and we have such a `const` move ctor). Well, C++14 solved this issue with [generalized lambda captures](https://isocpp.org/wiki/faq/cpp14-language#lambda-captures). – vsoftco Apr 19 '17 at 02:46
  • @vsoftco: So are you really asking why `const T&&` is deemed a move constructor? – Lightness Races in Orbit Apr 19 '17 at 13:37
  • I was mostly asking whether anyone bumped into a real-world usage scenario with such a move constructor. But I guess there is none. – vsoftco Apr 19 '17 at 14:16
  • @vsoftco: Nah can't think of one, much like writing a copy constructor as `T(T&)` is basically always silly – Lightness Races in Orbit Apr 19 '17 at 15:16
2

Not sure whether it's practical, but it can be made legal provided the modified data members are mutable.

This program is legal, and if you like that kind of thing, would easily become hard to follow:

#include <iostream>
#include <string>

struct animal
{
    animal(const animal&& other) : type(other.type) {
        other.type = "dog";
    }
    animal() = default;

    mutable std::string type = "cat";
};

std::ostream& operator<<(std::ostream& os, const animal& a)
{
    return os << "I am a " << a.type;
}
std::ostream& operator<<(std::ostream& os, const animal&& a)
{
    return os << "I am a " << a.type << " and I feel moved";
}

int main()
{
    const auto cat = animal();
    std::cout << cat << std::endl;

    auto dog = std::move(cat);
    std::cout << cat << std::endl;

    std::cout << dog << std::endl;
    std::cout << std::move(dog) << std::endl;
}

expected output:

I am a cat
I am a dog
I am a cat
I am a cat and I feel moved
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
1

As the comments have noted, you cannot actually "move" anything out of the argument object, because it is const (at least, not without a const cast, which is a bad idea as it could lead to UB). So it's clearly not useful for the sake of moving. The entire purpose of move semantics is to provide a performance optimization, and that is not happening here, so why do it?

That said, I can only think of two cases where this is useful. The first involves "greedy" constructors:

#include <iostream>

struct Foo {
    Foo() = default;
    Foo(const Foo&) { std::cerr << "copy constructor"; }
    Foo(Foo&&) { std::cerr << "copy constructor"; }

    template <class T>
    Foo(T&&) { std::cerr << "forward"; }      
};

const Foo bar() { return Foo{}; }

int main() {
    Foo f2(bar());        
    return 0;   
}

This program prints "forward". The reason why is because the deduced type in the template will be const Foo, making it a better match. This also shows up when you have perfect forwarding variadic constructors. Common for proxy objects. Of course returning by const value is bad practice, but strictly speaking it's not incorrect, and this may break your class. So you should really provide a Foo(const Foo&&) overload (which just delegates to the copy constructor); think of it as crossing a t or dotting an i when you are writing high quality generic code.

The second case occurs when you want to explicitly delete move constructors, or a move conversion operator:

struct Baz {
    Baz() = default;
    Baz(const Baz&) = default;
    Baz(Baz&&) = delete;
};

const Baz zwug() { return {}; }

int main() {
    Baz b2(zwug());
}

This program compiles so the author failed at their mission. The reason why is because const ref overloads match against const rvalues, and const rvalue construction was not explicitly deleted. If you want to delete moves you'll need to delete the const rvalue overload too.

The second example may seem wildly obscure but say you are writing a class that provides a view of a string. You may not want to allow it to be constructed from a string temporary, since you are at greater risk of the view being corrupted.

Nir Friedman
  • 17,108
  • 2
  • 44
  • 72
  • 1
    I don't find either case persuasive. The first example is better addressed by constraining the constructor template on `!std::is_same_v, Foo>`. Otherwise you'd also have to add a `Foo(Foo&);`, not to mention `volatile` headaches. The claimed use case in the second case doesn't concern move constructors, and in general it makes scant sense to provide a copy constructor but delete the move constructor. Moreover, for disabling rvalues, one typically just deletes `Foo(const Bar&&);`; deleting `Foo(Bar&&);` on top of that is superfluous. – T.C. Apr 18 '17 at 22:24
  • @T.C. With regards to the first, that's a good point, though with a variadic pack (which is more common) your solution would be uglier. With regards to the second, the question does not restrict itself to move constructors. But yes, you are right that it would be better to simply delete the classy rvalue overload, but that doesn't change my example really. – Nir Friedman Apr 19 '17 at 06:41