Skip to content

Chapter 5: Essential Operations

  • essential operations: construct/destruct/resource management
  • conventional opeartions: comparison, container, I/O

5.1

5.1.1

There are five situations in which an object can be copied or moved

  • As the source of assignment
  • As an object initializer
  • As a function argument
  • As a function return value
  • As an exception
struct X{
    X(Sometype);  // "ordinary constructor": create an object
    X();          // default constructor
    X(const X&);  // copy constructor
    X(X&&);       // move constructor
    X& operator=(const X&);  // copy assignment: clean up target and copy
    X& operator=(X&&);       // move assignment: clean up target and move
    ~X();         // destructor: clean up
};

struct Y{
    Y(const Y&) = default;  // really do want to use default copy constructor
    Y(Y&&) = default;  // and the default move constructor
};

If you are explicit about some defaults, other default definition will NOT be generated.

When a class has a pointer member, it is usually a good idea to be explicit about copy and move operations. The reason is that a pointer may point to something that the class needs to delete, in which case the default memberwise copy would be wrong. Alternatively, it might point to something that the class must not delete. In either case, a reader of the code would like to know.

A good rule of thumb (sometimes called the rule of zero) is to either define all of the essential operations or none (using the default for all.

To completement =default, we have =delete to indicate that an operation is not to be generated. A base class in a class hierarchy is the classical example where we don't want to allow a memberwise copy. =delete can be used to suppress any function, not just essential member functions.

5.1.2

To avoid implicit version, use explicit in constructor

class Vector {
public:
    explicit Vecotr(int size);  // no implicit conversion from int.
};

5.1.3

When a data member of a class is defined, we can supply a default initializer called a default member initializer.

class complex {
    double re = 0;
    double im = 0;  // default value
public:
    complex(double r, double i): re{r}, im{i} {}
    complex(double r): re{r} {} // im uses default value 0.0

};

5.2

By default, objects can be copied. This is true for objects of user-defined types as well as for built-in type. The default meaning of copy is memberwise copy: copy each member.

For simple concrete type, memberwise copy is often exactly the right semantics for copy. For some sophisticated concrete types, such as Vector, memberwise copy is not the right semantics for copy; for abstract type it almost never is.

5.2.1

WHen a class is a resource handle, the default memberwise copy is typically a disaster. The default copy would leave a copy of a vector referring to the same elements as the original. (this is the case for customized vector, but not necessarily std::vector)

Copying of an object of a class is defined by two members: a copy constructor and a copy assignment. It will be very dangerous to share the element due to copy constructor.

5.2.2

Given the definition, the compiler will choose the move constructor to implement the transfer of the return value out of the function.

Vector::Vector(Vector&& a): elem{a.elem}, sz{a.sz} {
    a.elem = nullptr;
    a.sz = 0;
}
The && means "rvalue reference" and is a reference to which we can bind an rvalue. The word "rvalue" is intended to complement "lvalue", which roughtly means "somethign that can appear on the left-hand side of an assignment." So an rvalue is -- to a first approximation -- a value that you can't assign to, such as an integer returned by a function call. Thus, an rvalue reference is a reference to something that nobody else can assign to, so we can safely "steal" its value.

A move constructor does not take a const argument: after all, a move constructor is supposed to remove the value from its argument. A move assignment is defined similarly.

A move opeartion is applied when an rvalue reference is used as an initializer or as the righthand side of an assignment.

The standard-library function move() doesn't actually move anything. Instead, it returns a reference to its argument from which we may move -- an rvalue reference; it is a kind of cast.

The compiler is obliged (by the C++ standard) to eliminate most copies associated with initialization, so move constructor are note invoked as often as you might imagine. This copy elision eliminates even the very minor overhead of a move. On the other hand, it is typically not possible to implicitly eliminate copy or move operations from assignments, so move assignments can be critical for performance.

5.3

Resource handles, such as Vector and thread, are superior alternatives to direct use of built-in pointers in many cases. In fact, the standard-library "smart pointers," such as unique_ptr, are themselves resource handle.

In very much the same way that new and delete disappear from application code, we can make pointers disappear into resource handles. In both cases, the result is simpler and more maintainable code, without added overhead. In particular, we can achieve strong resource safety; that is, we can eliminate resource leaks for a genral notion of a resource. Examples are vectors holding memory, threads holding system threads, and fstreams holding file handles.

My ideal is not to create any garbage, thus eliminating the need for a garbage collector: do not litter!

Garbage collection is fundamentally a global memory management scheme. Clever implementation can compensate, but as systems are getting more distributed, locality is more important than ever. (use RAII)

5.4

5.4.1

When defining ==, also define != and make sure that a!=b means !(a==b)

To give identical tratment to both operands of a binary operator, it is best defined as a free-standing function in the namespace of its class.

6.5.1 Comparison from 3rd edition

Like C's strcmp(), <=> implements a three-way-comparison. A negative return value means less-than, 0 means equal, and a positive value means greater-than.

If <=> is defined as non-default, == is not implicitly defined, but < and the other relational operators are!`

struct R2{
  int m;
  auto operator<=>(const R2& a) const { return a.m == m ? 0 : a.m < m ? -1 : 1; }  
};
void user(R2 r1, R2 r2)
{
    bool b4 = (r1 == r2);  // ERROR: no non-default ==
    bool b5 = (r2 < r2);   // OK.
}

This leads to this pattern of definition of nontrivial types:

struct R3 {};

auto operator<=>(const R3& a, const R3& b) { /*...*/ }
bool operator==(const R3& a, const R3& b) { /*...*/ }

Most standard-library types, such as string and vector, follow that pattern. The reason is that if a type has more than one element taking part in a comparison, the default <=> examines them one at a time yielding a lexicographical order. In such case, it is often worthwhile to provide a separate optimized == in addition because <=> has to examine all elements to determine alll three alternatvies.

5.4.2

containers has .size(), .begin(), .end()

5.4.3

<< and >> means output and input.

5.4.4

user define literals

library header namespace literals
<chrono> std::literals::chrono_literals h, min, s, ms, us, ns
<string> std::literals::string_literals s
<string_view> std::literals::string_literals sv
<complex> std::literals::complex_literals i, il, if

Unsurprisingly, literals with user-defined suffixes are called user-defined literals or UDLs. Such literals are defined using literal operators.

constexpr complex<double> operator""i(long double arg){ 
    return {0, arg}; 
}
  • The operator"" indicates that we are defining a literal operator
  • The i after the literal indicator "" is the suffix to which the operator gives a meaning.
  • The argument type, long double, indicates that the suffix (i) is being defined for a floating-point literal
  • The return type, complex<double>, specifies the type of the resulting literal

5.4.5

Many algorithms, most notably sort(), use a swap() function that excahgnes the values of two objects. It generally assumes that swap() is very fast and doesn't throw an exception. The standard-library provides a std::swap(a,b) implemented as three move operations: (tmp=a, a=b, b=tmp). If you design a type that is expensive to copy and could plausibly be swapped, then give it move operations or a swap() or both.

5.4.6

for a type X to be a key to std::unordered_map<K, V>, we must define hash<X>.

5.5

  • Return containers by value (relying on move for efficiency)
  • If a class is a resource handle, it needs a user-defined constructor, a destructor, and non-default copy operations.

TODO