Herramientas de usuario

Herramientas del sitio


wiki2:cpp:idiomatic

Idiomatic C++

Idioms

POD types

POD type (Plain Old Data, also Passive data structure) is, a class (whether defined with the keyword struct or the keyword class) without constructors, destructors and virtual members functions.

In other words a POD is a type where the C++ compiler guarantees that there will be no “magic” going on in the structure. Roughly speaking, a type is a POD when the only things in it are built-in types and combinations of them. The result is something that “acts like” a C type.

The STD has a method to say if a type is a POD:

// is_pod example
#include <iostream>
#include <type_traits>
 
struct A { int i; };            // C-struct (POD)
class B : public A {};          // still POD (no data members added)
struct C : B { void fn(){} };   // still POD (member function)
struct D : C { D(){} };         // no POD (custom default constructor)
 
int main() {
  std::cout << std::boolalpha;
  std::cout << "is_pod:" << std::endl;
  std::cout << "int: " << std::is_pod<int>::value << std::endl;
  std::cout << "A: " << std::is_pod<A>::value << std::endl;
  std::cout << "B: " << std::is_pod<B>::value << std::endl;
  std::cout << "C: " << std::is_pod<C>::value << std::endl;
  std::cout << "D: " << std::is_pod<D>::value << std::endl;
  return 0;
}

This code results:

is_pod:
int: true
A: true
B: true
C: true
D: false

The rule of three and the copy-and-swap idiom

There are two ways to copy an object in C++: the copy constructor or with the assignment.

class person
{
    std::string name;
    int age;
 
public:
 
    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};
 
int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}

The rule of three says that the copy constructor and copy assignment operator, and destructor are special member functions. The implementation will implicitly declare these member functions for some class types when the program does not explicitly declare them. The implementation will implicitly define them if they are used.

The implicit definition wolud be:

person(const person& that) : name(that.name), age(that.age)
{
}
 
// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}
 
// 3. destructor
~person()
{
}

However when they manage resources (pointers and so) it's not so straightforward:

class person
{
    char* name;
    int age;
 
public:
 
    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }
 
    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};

However this can bring some unpleasant effects:

  1. Changes via a can be observed via b.
  2. Once b is destroyed, a.name is a dangling pointer.
  3. If a is destroyed, deleting the dangling pointer yields undefined behavior.
  4. Since the assignment does not take into account what name pointed to before the assignment, sooner or later you will get memory leaks all over the place.

There is a naive implementation that solves these problems:

person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}
 
// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}

Or you also can make those resources non copiable:

private:
    person(const person& that);
    person& operator=(const person& that);

copy-and-swap idiom

The copy-and-swap idiom is the solution, and elegantly assists the assignment operator in achieving two things: avoiding code duplication, and providing a strong exception guarantee.

Conceptually, it works by using the copy-constructor's functionality to create a local copy of the data, then takes the copied data with a swap function, swapping the old data with the new data. The temporary copy then destructs, taking the old data with it. We are left with a copy of the new data.

In order to use the copy-and-swap idiom, we need three things: a working copy-constructor, a working destructor (both are the basis of any wrapper, so should be complete anyway), and a swap function.

A swap function is a non-throwing function that swaps two objects of a class, member for member. We might be tempted to use std::swap instead of providing our own, but this would be impossible; std::swap uses the copy-constructor and copy-assignment operator within its implementation, and we'd ultimately be trying to define the assignment operator in terms of itself!

Let's consider a concrete case. We want to manage, in an otherwise useless class, a dynamic array. We start with a working constructor, copy-constructor, and destructor:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t
 
class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : 0)
    {
    }
 
    // copy-constructor
    dumb_array(const dumb_array& other) 
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : 0),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }
 
    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }
 
private:
    std::size_t mSize;
    int* mArray;
};

This class almost manages the array successfully, but it needs operator= to work correctly.

Here's how a naive implementation might look:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = 0; // (2) *(see footnote for rationale)
 
        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : 0; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }
 
    return *this;
} 

And we say we're finished; this now manages an array, without leaks. However, it suffers from three problems, marked sequentially in the code as (n).

(1) The first is the self-assignment test. This check serves two purposes: it's an easy way to prevent us from running needless code on self-assignment, and it protects us from subtle bugs (such as deleting the array only to try and copy it). But in all other cases it merely serves to slow the program down, and act as noise in the code; self-assignment rarely occurs, so most of the time this check is a waste. It would be better if the operator could work properly without it.

(2)The second is that it only provides a basic exception guarantee. If new int[mSize] fails, *this will have been modified. (Namely, the size is wrong and the data is gone!) For a strong exception guarantee, it would need to be something akin to:

dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get the new data ready before we replace the old
        std::size_t newSize = other.mSize;
        int* newArray = newSize ? new int[newSize]() : 0; // (3)
        std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
 
        // replace the old data (all are non-throwing)
        delete [] mArray;
        mSize = newSize;
        mArray = newArray;
    }
 
    return *this;
} 

(3) The code has expanded! Which leads us to the third problem: code duplication. Our assignment operator effectively duplicates all the code we've already written elsewhere, and that's a terrible thing.

We want this:

class dumb_array
{
public:
    // ...
 
    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap; 
 
        // by swapping the members of two classes,
        // the two classes are effectively swapped
        swap(first.mSize, second.mSize); 
        swap(first.mArray, second.mArray);
    }
 
    // ...
};

With…

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)
 
    return *this;
} 

e first notice an important choice: the parameter argument is taken by-value. While one could just as easily do the following (and indeed, many naive implementations of the idiom do):

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);
 
    return *this;
}

The next version of C++, C++11, makes one very important change to how we manage resources: the Rule of Three is now The Rule of Four (and a half). Why? Because not only do we need to be able to copy-construct our resource, we need to move-construct it as well.

Luckily for us, this is easy:

class dumb_array
{
public:
    // ...
 
    // move constructor
    dumb_array(dumb_array&& other)
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }
 
    // ...
};

Value categories

Main C++ expression types are rvalue and lvalue. It depends on which part of an assignment they appear (right-hand: rvalue, left-hand: lvalue).

Mainly, an lvalue refers to an object that persists beyond a single expression (all variables, including nonmodifiable (const) variables, are lvalues), they are also called locator value because represents an object that occupies some identifiable location in memory. An rvalue is a temporary value that does not persist beyond the expression that uses it.

In the next example, x is an lvalue because it persists beyond the expression that defines it. The expression 3 + 4 is an rvalue because it evaluates to a temporary value that does not persist beyond the expression that defines it.

#include <iostream>
using namespace std;
int main()
{
   int x = 3 + 4;
   cout << x << endl;
}
 // Incorrect usage: The left operand must be an lvalue (C2106).
7 = i; // C2106
j * 4 = 7; // C2106
 
// Correct usage: the dereferenced pointer is an lvalue.
*p = i; 
 
const int ci = 7;
// Incorrect usage: the variable is a non-modifiable lvalue (C3892).
ci = 9; // C3892
 
// Correct usage: the conditional operator returns an lvalue.
((i < 3) ? i : j) = 7;
 

An xvalue (an “eXpiring” value) also refers to an object, is the result of certain kinds of expressions involving rvalue references.

Since C++11 the rvalue is named prvalue (pure rvalue), and is redefined a an rvalue that is not an xvalue.

There also are the glvalue type expression (generalized lvalue) which are an lvalue or an xvalue.

RAII

Precompiled headers

Forward declaration

Pimpl

Keywords

const and mutable

const over #define: const is typed, #define macros are not. const is scoped by C block, #define applies to a file.

const is most useful with parameter passing. If you see const used on a prototype with poiinters, you know it is safe to pass your array or struct because the function will not alter it. No const and it can.

void myfunc(const char x);

This means that the parameter x is a char whose value cannot be changed inside the function. For example:

void myfunc(const char x)
{
  char y = x;  // OK
  x = y;       // failure - x is `const`
}

const member functions prevent modification of any class member.

int myclass::myfunc() const
{
  // do stuff that leaves members unchanged
}

If you have specific class members that need to be modifiable in const member functions, you can declare them mutable.

const char* p is a pointer to a const char. char const* p is a pointer to a char const. However, consider: char * const p is a const pointer to a (non-const) char. I.e. you can change the actual char, but not the pointer pointing to it.

int main
{
 const int i = 10;
 const int j = i+10;  // Works fine
 i++;    // This leads to Compile time error   
}

This means that the pointer is pointing to a const variable: const int* u;

To make the pointer const, we have to put the const keyword to the right of the *.

int x = 1;
int* const w = &x;

Here, w is a pointer, which is const, that points to an int. Now we can't change the pointer but can change the value that it points to.

We can also have a const pointer pointing to a const variable: const int* const x;

class Test
{
 const int i;
 public:
 Test (int x)
 {
   i=x;
 }
};
 
int main()
{
 Test t(10);
 Test s(20);
}

In this program, i is a const data member, in every object its independent copy is present, hence it is initialized with each object using constructor. Once initialized, it cannot be changed.

When an object is declared or created with const, its data members can never be changed, during object's lifetime.

const class_name object;

Always use const for function parameters passed by reference where the function does not modify (or free) the data pointed to: int find(const int *data, size_t size, int value);

Never use const in a function prototype for a parameter passed by value. It has no meaning and is hence just 'noise'.

Where appropriate, use const volatile on locations that cannot be changed by the program but might still change. Hardware registers are the typical use case here, for example a status register that reflects a device state.

Resources

Libraries

Notes

  • “Slicing” is where you assign an object of a derived class to an instance of a base class, thereby losing part of the information - some of it is “sliced” away.

Speed up compiler times

Clockwise spiral rule to read C++ expressions

     +-------+
     | +-+   |
     | ^ |   |
char *str[10];
 ^   ^   |   |
 |   +---+   |
 +-----------+

What is str?

  1. str is an…
  2. str is an array 10 of…
  3. str is an array 10 of pointers to…
  4. str is an array 10 of pointers to char
wiki2/cpp/idiomatic.txt · Última modificación: 2020/05/09 09:25 (editor externo)