Are there any use cases for a class which is copyable but not movable?
Asked Answered
P

6

18

After reading this recent question by @Mehrdad on which classes should be made non-movable and therefore non-copyable, I starting wondering if there are use cases for a class which can be copied but not moved. Technically, this is possible:

struct S
{
    S() { }
    S(S const& s) { }
    S(S&&) = delete;
};

S foo()
{
    S s1;
    S s2(s1); // OK (copyable)
    return s1; // ERROR! (non-movable)
}

Although S has a copy constructor, it obviously does not model the CopyConstructible concept, because that is in turn a refinement of the MoveConstructible concept, which requires the presence of a (non-deleted) move constructor (see § 17.6.3.1/2, Table 21).

Is there any use case for a type like S above, which is copyable but not CopyConstructible and non-movable? If not, why is it not forbidden to declare a copy constructor and a deleted move constructor in the same class?

Pamella answered 14/1, 2013 at 17:4 Comment(10)
i think your S is not copyable because copying from an rvalue fails. and in general what do you mean by "movable"? generally, you can say that a copy is also a move, because the latter leaves the source object in an unspecified state. and not changing the source state satisfies that.Poverty
The latter question is easy: just because nobody can find a use now doesn't mean there doesn't exist one, and even if there truly doesn't a use then we'll simply not code it. Why does the langauge text need to expand to forbid something we'll never do?Equi
@JohannesSchaub-litb: good point. what I mean is whether it does have a sense to have a copy constructor and a deleted move constructor in the same class, whether there are use cases for this and, if not, why it is not forbidden.Pamella
@GManNickG: typically in order to prevent us doing it by accident. "We don't need const", they said, "we'll just not modify objects that we're not supposed to".Condescend
@GManNickG: ok, sounds like a reasonable answer to the last questionPamella
@SteveJessop: also a good pointPamella
@JohannesSchaub-litb When you move an object, it must remain in a defined state, because the destructor will be called nevertheless.Foreconscious
@SteveJessop: Modifying an object you're not suppose to is a bad thing, so we have const to help the compiler enforce that. This is not the same thing (I suspected I would get a reply like that). Explicitly declaring that you want a copyable but not movable class? What's bad about that except it may just be unnecessary? In fact, your answer seems to just support what I said...Equi
@GManNickG: Agreed. I was responding specifically to your question, "Why does the language text need to expand to forbid something we'll never do?". The answer is that just because something is always pointless and harmful, doesn't really mean we'll never do it, it just means we don't want to do it. If this were always pointless and harmful, then diagnostics would be in order. But like you say, that's not certain. So in my answer I've tried to imagine a possible (if somewhat flaky) use.Condescend
@SteveJessop: Fair enough. :)Equi
C
13

Suppose you have a class that is no cheaper to move than it is to copy (perhaps it contains a std::array of a POD type).

Functionally, you "should" make it MoveConstructible so that S x = std::move(y); behaves like S x = y;, and that's why CopyConstructible is a sub-concept of MoveConstructible. Usually if you declare no constructors at all, this "just works".

In practice, I suppose that you might want to temporarily disable the move constructor in order to detect whether there is any code in your program that appears more efficient than it really is, by moving instances of S. To me it seems excessive to forbid that. It's not the standard's job to enforce good interface design in completed code :-)

Condescend answered 14/1, 2013 at 17:11 Comment(4)
ok, I'm not sure I understand what you mean by "appears more efficient than it really is". Do you mean that you want to detect all the parts of the code where move construction is used explicitly under the false assumption that it will lead to better performance and fix those instructions?Pamella
@AndyProwl: I'm not going so far as to say that I want to do that, because I never actually have and I might have overlooked some reason why it's not helpful. But basically yes, I'm imagining that I want to ensure that I haven't written any copies that "look like" moves. What I'm not sure, though, is what I'd do if I found one. For example the move-return in your function foo, the only way to stop it being a move is to make it ineligible for copy-elision, which seems like a bad idea given that copies are expensive! That's why I said only temporarily disable moves, it's a crude tool.Condescend
Makes sense, thank you for clarifying. I think I will accept this answer, although @GManNickG's argument "if it doesn't hurt, why forbidding it?" is also a valid an answer to my last question. In fact, I must admit I do not see a way it could do damage.Pamella
Actually come to think of it, there is a really outlandish reason why I might want to disable the move in order to find points in my code that return S by move-construction. If I'm porting to a compiler that doesn't do NRVO, I want to rewrite those functions. That compiler is an abomination: C++11 compilers are required to detect when copy-elision is permitted, so why not actually perform it? But maybe there's some deeply unwise calling convention out there, that prevents it ;-) I don't claim that this horrible task is a positive reason for the standard to permit S, mind you.Condescend
P
10

I currently know of no use cases for a deleted move constructor/assignment. If done carelessly it will needlessly prevent a type from being returned from a factory function or put into a std::vector.

However deleted move members are legal nevertheless just in case someone might find a use for them. As an analogy, I knew of no use for const&& for years. People asked me if we shouldn't just outlaw it. But eventually a few use cases did show up after we got sufficient experience with the feature. The same might also happen with deleted move members, but to the best of my knowledge hasn't yet.

Pironi answered 14/1, 2013 at 20:12 Comment(5)
This sounds pretty amazing to me, but it is my lack of experience that speaks. Why should we trust the compiler generated move operations by default, rather than deleting them by default ? Its nice to give ability to types, but they need to be proven. If you consistently use RAII and "rule of zero", you're good I guess. It seems to me somehow that you imply that default move behavior is safe in almost every case, without thinking about it (except mutex or odd behaved). But still move for a POD would just be a copy, what if you want to forbid copies to guarantee performance; at compile time ?Test
Been a few years, but if I'm thinking of the same thing as you, there's a million reasons to delete a move constructor. Imagine trying to move a mutex; the headache.Pelt
One can make a non-movable/non-copyable object by simply deleting the copy members. Deleted move members for such an object are redundant. This Q/A concerns an object that is copyable but not movable. mutex doesn't qualify as that.Pironi
(on const&&) But eventually a few use cases did show up after we got sufficient experience with the feature. - What are the use cases?Harmattan
When I wrote this answer a decade ago, the only uses I knew of were for std::ref and std::cref, which has deleted signatures using const&&. These were added for the purpose of eliminating dangling references at compile-time. Since then uses of const&& have been added in several more place ranging from tuple, to optional to variant, to ranges. And they are not all associated with deleted signatures. I have not analyzed the precise purpose of each of those uses. But it is clear that if const&& was changed to be a compile-time error, it would break existing code in the std::lib.Pironi
F
4

I don't think there can be any reasonable class which would prevent move, yet allow copy. It is clear from the very same topic, that move is just an efficient way of copy when you don't need the original object anymore.

Fredi answered 14/1, 2013 at 17:9 Comment(0)
A
2

I was looking at this very issue today, because we've ported some code from VS2005 into VS2010 and started seeing memory corruption. It turned out to be because an optimization (to avoid copying when doing a map lookup) didn't translate into C++11 with move semantics.

class CDeepCopy
{
protected:
    char* m_pStr;
    size_t m_length;

    void clone( size_t length, const char* pStr )
    {
        m_length = length;
        m_pStr = new char [m_length+1];
        for ( size_t i = 0; i < length; ++i )
        {
            m_pStr[i] = pStr[i];
        }
        m_pStr[length] = '\0';
    }

public:
    CDeepCopy() : m_pStr( nullptr ), m_length( 0 )
    {
    }

    CDeepCopy( const std::string& str )
    {
        clone( str.length(), str.c_str() );
    }

    CDeepCopy( const CDeepCopy& rhs )
    {
        clone( rhs.m_length, rhs.m_pStr );
    }

    CDeepCopy& operator=( const CDeepCopy& rhs )
    {
        if (this == &rhs)
            return *this;

        clone( rhs.m_length, rhs.m_pStr );
        return *this;
    }

    bool operator<( const CDeepCopy& rhs ) const
    {
        if (m_length < rhs.m_length)
            return true;
        else if (rhs.m_length < m_length)
            return false;

        return strcmp( m_pStr, rhs.m_pStr ) < 0;
    }

    virtual ~CDeepCopy()
    {
        delete [] m_pStr;
    }
};

class CShallowCopy : public CDeepCopy
{
public:

    CShallowCopy( const std::string& str ) : CDeepCopy()
    {
        m_pStr = const_cast<char*>(str.c_str());
        m_length = str.length();
    }

    ~CShallowCopy()
    {
        m_pStr = nullptr;
    }
};

int _tmain(int argc, _TCHAR* argv[])
{
    std::map<CDeepCopy, int> entries;
    std::string hello( "Hello" );

    CDeepCopy key( hello );
    entries[key] = 1;

    // Named variable - ok
    CShallowCopy key2( hello );
    entries[key2] = 2;

    // Unnamed variable - Oops, calls CDeepCopy( CDeepCopy&& )
    entries[ CShallowCopy( hello ) ] = 3;

    return 0;
}

The context was that we wanted to avoid unnecessary heap allocations in the event that the map key already existed - hence, the CShallowCopy class was used to do the initial lookup, then it would be copied if this was an insertion. The problem is that this approach doesn't work with move semantics.

Amadis answered 15/1, 2013 at 17:4 Comment(0)
E
0

It depends on how you define the semantics of the move operation for your type. If move merely means optimized copy through resource-stealing then the answer is probably no. But the answer might be yes if move means "relocate" in the sense used by a moving garbage collector or some other custom memory management scheme.

Consider a real-world example of a house located on a particular street address. One can define a copy of that house to be another house built using the exact same blueprints, but located on another address. Under these terms, we can go on saying that a house cannot be moved, because there might be people referring to it by its address. Translating to technical terms, the move operation might not be possible for structures that have inbound pointers.

I can imagine a bit twisted implementation of a signals/slots library that allows its signals objects to be copied, but doesn't allow them to be moved.

disclaimer: some C++ purists will point out that STL (and thus the standard) defines what a move operation is and it's not in line with what I described here so I won't argue with that.

Evetteevey answered 15/1, 2013 at 11:1 Comment(0)
D
-1

Take const qualified objects: You can copy (from) them (unless there are other reasons why you cannot), but moving (from) them is not meaningfully possible because for any kind of actual transfer, the moved-from object must be mutated.

This reasoning extends to classes that are const conceptually, but aren’t actually const qualified.

Diadelphous answered 28/12, 2022 at 1:6 Comment(1)
Your example doesn't answer the question. Deleting the move-constructor of a T type only prevents T&& values; but moving a const qualified object forms a const T&& which will match the const T& constructor from rvalue-to-const-lvalue conversion. It doesn't catch your hypothetical example. The question is not about deleting const-Rvalue constructors, which would catch this, but is not a "move constructor".Orton

© 2022 - 2024 — McMap. All rights reserved.