1

I am writing a c++11+ standards compliant class, and it is time for me to implement the rule of 5.

  1. Destructor
  2. Copy Constructor
  3. Move Constructor
  4. Copy Assignment Operator
  5. Move Assignment Operator

I had a question about copy/move constructors/assignment. It is my understanding that a copy constructor/assignment should make a copy (shallow, deep?) of your class. In the case that your class has unique members, such as a unique_ptr, there are two scenarios I foresee.

  • Make a deep copy of the object

    I am not sure, in my case, how I would make a deep copy (see code below).

  • Move the object to the other class

    In my opinion, moving the pointer in a copy constructor has unintended side effects for a user, as they are expecting a copy, and not a move, and the original object being copied would no longer function.

    Making a copy could also be problematic, however, in my case, the curl object could contain sensitive information such as cookies or a password?


What is the defacto way to create copy and move constructors/assignment for classes with these constraints? To deep copy, to move, or not explicitly and implicitly define a copy constructor (does the delete keyword do this)?


// client.h
#pragma once

#include <Poco/URI.h>
#include <curl/curl.h>

class client
{
public:
    typedef Poco::URI uri_type;
    // Constructor
    client(const uri_type & auth);
    // Destructor
    virtual ~client();
    // Copy constructor
    client(const client & other);
    // Move constructor
    client(client && other);
    // Copy assignment
    client & operator=(const client & other);
    // Move assignment operator
    client & operator=(client && other);

private:
    uri_type auth_;
    // ... other variables (both unique and copyable) ommitted for simplicity.
    std::unique_ptr<CURL, void(*)(CURL*)> ptr_curl_;
};

// client.cpp
#include <memory>
#include <Poco/URI.h>
#include <curl/curl.h>

#include "client.h"

// Constructor
client::client(const uri_type & auth)
: auth_(auth)
, ptr_curl_(curl_easy_init(), curl_easy_cleanup)
{
    curl_global_init(CURL_GLOBAL_DEFAULT);
}

// Destructor
client::~client()
{
    curl_global_cleanup();
}

// Copy constructor
client::client(const client & other)
{
    // ... deep copy? move?
    // how would you deep copy a unique_ptr<CURL>?
}

// Move constructor
client::client(client && other)
{
    std::swap(*this, other);
}

// Copy assignment
client & client::operator=(const client & other)
{
    // Cant get this to work by making the copy happen in the parameter.
    client temp(other);
    std::swap(*this, temp);
    return *this;
}

// Move assignment operator
client & client::operator=(client && other)
{
    return *this;
}
Francisco Aguilera
  • 3,484
  • 6
  • 23
  • 49
  • 3
    try to follow the rule of zero if possible! http://flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html – vsoftco Apr 06 '15 at 04:58
  • 3
    As a guideline, when unsure whether to support move or copy in some more complex situation like this, start by making your class non-copyable, then let the needs of the the client code - as they become clearer - drive whether you add support for copying and/or moving, and with what exact semantics, i.e. shallow vs deep, reestablishing the connection/cookies etc.. If you can't clearly see what the client needs to work, it's too early to complete the design. – Tony Delroy Apr 06 '15 at 05:42
  • @vsoftco I disagree. People who follow the rule of 0 make me have to implement 5 methods for them anytime I want to debug...and if you think you won't be debugging to find out when a particular class got constructed, give your career a few more years. – Christopher Pisz Nov 27 '17 at 22:59
  • @ChristopherPisz In that case I'd just implement a base class that keeps track of objects (i.e. counter, construction/destruction) then just derive from it in Debug mode (like `class MyClass #ifdef DEBUG : public Counter #endif`), see e.g. https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern#Object_counter. I still believe that code should be kept as simple as possible and one should re-use as much as possible. – vsoftco Nov 29 '17 at 15:50
  • @vsoftco that really seems counter to the idea of keeping it as simple as possible. you are in fact introducing yet more code on top of the compiler generated code to accomplish a debugging task. See related discussion here: https://softwareengineering.stackexchange.com/questions/361392/if-you-follow-the-rule-of-zero-how-will-you-debug-the-construction-of-an-object/361442#361442 I've been on several topics related to the rle of zero this week :) – Christopher Pisz Nov 29 '17 at 15:59
  • @ChristopherPisz I'm introducing only one class that I reuse everywhere, I find it easier than writing ctors/dtors/move/copy operators for every single class. But again, this may be a matter of personal preference. – vsoftco Nov 29 '17 at 16:00
  • @vsoftco right on. by no means am I saying you shouldn't do that. However, that particular solution doesn't give me the warm fuzzy. – Christopher Pisz Nov 29 '17 at 16:29

2 Answers2

4

As the name indicates, a copy constructor/assignment operator should always COPY and not move its members, where copy usually means deep copy.

Remember: By default, all objects in c++ should have value semantics, i.e. they should behave like an int.

Furthermore, the terminology in your post indicates that you are confusing unique objects (singletons) with objects pointed to by unique_ptr. Most non-copyable objects are handlers (like unique_ptr handles objects on the heap) in which case you'd copy whatever they handle. If that is not possible, then most likely, it doesn't make sense to implement a copy constructor of your object at all.

If your object holds an owning reference to a unique resource (from which there can be only one instance in your project),then the first question would be: can it be shared? -> use shared_ptr. If not -> don't copy. If your object holds a non-owning reference to a unique ressource (raw pointer or reference) copy the reference. In both cases, be aware that you now have two objects, that share part of their state, which might be dangerous even in non-multithreaded applications.

MikeMB
  • 17,569
  • 7
  • 51
  • 93
  • [Most other posts](http://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom) in stack overflow say to move within the copy constructor if its unique, that question needed clarification. – Francisco Aguilera Apr 06 '15 at 05:02
  • @FranciscoAguilera: Uh, where exactly does that say to implement a destructive copy constructor? It's a really long post, since you've already identified something that concerns you, it makes more sense for you to share it than for me to read the whole page again. – Ben Voigt Apr 06 '15 at 05:05
  • @FranciscoAguilera: That is about how to implement the copy, not whether to move OR copy – MikeMB Apr 06 '15 at 05:06
  • @BenVoigt Well isn't that what swap is doing in essence? When I think of swapping something in real life you lose an object and get a different object, same deal as moving? – Francisco Aguilera Apr 06 '15 at 05:07
  • Also, my question would still fall down to whether to create a deep copy (possible security issues) or to disable copying by specifying delete. – Francisco Aguilera Apr 06 '15 at 05:09
  • @FranciscoAguilera: I think you saw "move" or "swap" and didn't consider the context. – Ben Voigt Apr 06 '15 at 05:09
  • @FranciscoAguilera: In particular, the method recommended in that page does not swap the item being copied. It makes a temporary copy (so the copy-assignment target isn't left corrupted if an exception is thrown) and then swaps with the temporary, leaving the original completely intact. This basically delegates the copy-assignment logic to the copy-constructor, preventing code duplication. – Ben Voigt Apr 06 '15 at 05:12
  • @FranciscoAguilera: No,the example doesn't swap the target with the surce, but rather the target with a local temporary copy. – MikeMB Apr 06 '15 at 05:13
  • @BenVoigt You are right, I think I got confused with unique_ptr semantics (how one moves the pointer to another instead of having two unique_ptr pointing to the same data) – Francisco Aguilera Apr 06 '15 at 05:15
  • @MikeMB It does for the move function :P But yeah I saw. – Francisco Aguilera Apr 06 '15 at 05:16
  • @FranciscoAguilera: Sorry, I thought we were talking about the copying. – MikeMB Apr 06 '15 at 05:25
1

For some types, having a copy constructor is inappropriate. That includes most types that semantically contain non-copyable types. (Don't count the unique_ptr, do count the CURL instance)

If libcurl has a "duplicate_handle" sort of function, then your copy constructor should use it to initialize the unique_ptr in the copy. Otherwise, you should delete your copy constructor and only implement move.

Ben Voigt
  • 260,885
  • 36
  • 380
  • 671
  • Well, if "The new handle will not inherit any state information, no connections, no SSL sessions and no cookies" is what you expect when you copy a `client` instance, then that's the way to go. – Ben Voigt Apr 06 '15 at 05:17
  • It states "All strings that the input handle has been told to point to (as opposed to copy) with previous calls to curl_easy_setopt using char * inputs, will be pointed to by the new handle as well. You must therefore make sure to keep the data around until both handles have been cleaned up." So therefore this wouldn't be a proper copy anyway... – Francisco Aguilera Apr 06 '15 at 05:20
  • Back to square one, I am still not sure whether to copy using duphandle or not allow copying by using delete in my specific case. – Francisco Aguilera Apr 06 '15 at 05:23
  • 1
    @FranciscoAguilera: The question you have to ask yourself is: Does it semantically sense (in your program) to copy a client object and what would you expect from that copy. If it doen't make sense to copy it, then delete the copy constructor and assignement operator. If it makes sense to have a copy, but the semantics are not obvious, then delete the copy functions and write a named copy function like (`createClientToSameServer()`). – MikeMB Apr 06 '15 at 05:29
  • @MikeMB This client uses curl to download said pages from a site. I could have a parallelized solution with multiple clients (copied from the original) downloading pages from the same site, but, curl says that "the new handle will not inherit any state information, no connections, no SSL sessions and no cookies." So it would be the equivalent of just creating a new client to the same site basically since it doesn't hold (any?) information. I would have to be inclined not to provide a copy constructor then. Does this logic seem sound to you? – Francisco Aguilera Apr 06 '15 at 05:40
  • 1
    @Francisco Aguilera: I'd say so too. – MikeMB Apr 06 '15 at 06:06