11.2. Copying objects

C++ is one of the few languages that provides precise control over how memory is managed. In C++11, every class is created with 5 special functions:

In many classes, such as most of those written so far, the compiler generated default versions work fine. As we will see, sometimes you have to write them yourself.

Programmers have choices on how (or if) objects are copied and moved. Whenever an object is passed by value to a function, or returned by value from a function, a copy is implicitly performed:

std::vector<int> scores;
auto avg = average (scores);  // a copy of scores is passed to average

Copy operations also occur in range-for loops:

for (const int value: scores)

Each member of scores is copied into value on each iteration.

Explicit copying can also be performed. Whenever you have an existing object and use it to initialize a new or existing object, the copy constructor is called:

std::vector<std::string> words;
std::vector<std::string> w2 = words; // copy words into w2

Both explicit and implicit copies are controlled by a special constructor called the copy constructor. Like other constructors, the copy constructor is a member function with the same name as the class name. The signature must be able to evaluate to this:

class_name ( const class_name & );

A copy constructor may take other parameters, but that is uncommon. If there are other parameters, they must all have defaults values defined. In general, the default copy constructor generated by the compiler will suffice. If the default creation is inhibited for any reason, it is acceptable to explicitly declare the default constructor:

class_name ( const class_name & ) = default;

However the default copy constructor is created, the behavior is the same: each class member is copied, in initialization order.

A simple class with a default and a copy constructor.

struct A
{
  int n;
  double d;

  // user defined default constructor
  A(int n = 0, double d = 1)
    : n{n}
    , d{d}
  { }

  // user defined copy constructor
  A(const A& other)
    : n{other.n}
    , d{other.d}
  { }
};

In this case, the user defined copy constructor does what the default constructors would do. When that is the case, it’s best not to redo the work of the compiler.

When objects manage their own resources, simple member-wise assignment cannot be used. Consider the following:

#include <cstddef>
#include <cstring>
#include <iostream>

namespace mesa {
  class string {
    private:
      char* data = nullptr;
      size_t sz = 0;

    public:
      string() = default;
      explicit string(const char* source) {
        // if source not null terminated, strlen behavior is undefined
        sz = std::strlen(source) + 1; // +1 for null terminator
        data = new char[sz];
        std::strncpy(data, source, sz);
      }

      void upper_case() {
        for (size_t i=0; i < sz; ++i) {
          data[i] = std::toupper(data[i], std::locale());
        }
      }

      ~string() {
        delete[] data;
      }

      char* c_str() const noexcept { return data; }
  };
} // namespace mesa

This class encapsulates an array of characters, providing 5 functions:

What happens when we use this class?

int main() {
  mesa::string hello("Hello, world!");
  mesa::string copy = hello;
  copy.upper_case();
  std::cout << hello.c_str() << '\n';
  std::cout << copy.c_str() << '\n';

  return 0;
}

Even though we copied hello, changing the case of copy also resulted in changes to the original. In this case, we only copied the pointer, not the data pointed to.

When we copy a value, we expect a cloned object. A object that in all respects has the same attributes, but that is separate and distinct. We don’t want changes in one to affect the other.

The default copy behavior is a shallow copy: a literal copying of the bytes of each member variable. In the case of our string class, the char* is faithfully copied. When the copy is made, both variables point to the same memory.

Activity: CodeLens 11.2.3 (codelens_shallow_copy_string_class)

Because there are two pointers to the same data on the free store, when either is deleted, the free-store memory is recovered. Consider this:

mesa::string hello("Hello, world!");

// create a new scope
{
  mesa::string copy = hello;
} // local variable copy destroyed

std::cout << hello.c_str() << '\n';

What does the last line print?

Fixing these problems requires writing a custom copy constructor.

string (const string& other) {
  sz = other.sz;
  data = new char[sz];
  std::strncpy(data, other.data, sz);
}

Each class member needs to be copied. The member sz can simply be default copied. It’s the pointer member that needs special treatment:

In contrast to a shallow copy, this copy is a deep copy. It doesn’t copy the pointer at all. It makes an entirely new pointer and (deeply) copies all of the data pointed to by the source pointer to the destination.

Try This!

Take the copy constructor provided and implement in the previous mesa::string examples in this section.

11.2.1. Copy assignment

The copy assignment operator is similar to the copy constructor. The key difference to remember is that a copy constructor is only called when the left hand side object does not yet exist: it is in the process of being constructed.

X& X::operator=(const X& other)
{
  // copy other content into this
  return *this;
}

Copy assignment is called when both already exist and you want to copy the right hand side object into the left hand side object.

struct A
{
  int n;
  double d;

  // user defined default constructor
  A(int n = 0, double d = 1)
    : n{n}
    , d{d}
  { }

  // user defined copy constructor
  A(const A& other)
    : n{other.n}
    , d{other.d}
  { }

  A& operator=(const A& other)
  {
    if (this == &other) { return * this; }
    n = other.n;
    d = other.d;
    return * this;
  }

};

Try This!

Write a copy assignment function for the mesa::string class.


More to Explore

You have attempted of activities on this page