Let yourself get used to C in effective c + +++

Posted by lisa3711 on Mon, 29 Jun 2020 06:58:29 +0200

Let yourself get used to C in effective c + +++

Label: effective C++

Item 1: treat C + + as a language Federation

  • C
  • Object-Oriented C++
  • Template C++
  • STL

C + + can be seen as a combination of four secondary languages. When you switch from a secondary language, efficient programming rules will require you to change your strategy.
For example:
In C-based design, built-in types are usually more efficient than pass by reference;
In O-O design, pass by reference const is more efficient because of the existence of user-defined constructors and destructors;
This is especially true with Template C + +, where you don't even know the type of object you're dealing with;
Once you step into STL, pass by value becomes applicable again, because the iterator and function object of STL are shaped on the C pointer.

Item 2: try to replace "define" with const, enum and inline

#define defines the identifier as a macro, and the preprocessing phase completes the text macro replacement. It is done by the preprocessor rather than by the compiler, which means that the compiler is preferred to replace the preprocessor

Not conducive to debugging

#define ASPECT_RATIO 1.653
ASPECT_RATIO = 1.5;

The old compiler does not record macro replacement information if aspect_ The error message will be 1.653 and confuse you. Of course, the error message of modern compiler will record the macro replacement information and tell you the error message about ask_ RATIO

Error prone

#define MAX(a,b) a>b?a:b
3 * MAX(5,6) //The result is 5 instead of 18 -- > 3 * 5 > 6? 5:6
#define DIVIDE(a,b) a/b
DIVIDE(3+5,2) //The result is 5 instead of 4 -- > 3 + 5 / 2

//Correct behavior
#define MAX2(a, b) ((a) > (b) ? (a) : (b))

However, in addition to the need for parentheses, "define" has other problems

#define MAX(a, b) (a) > (b) ? (a) : (b)
MAX(i++,j) //The number of i increments depends on j. does the caller know?

enum hack

When we need to use class specific constants, enum hack can be used as an alternative (int) to static const.

class A
{
private:
static const double value;
};
const double A::value = 1.1;

class B
{
private:
enum { value = 5};
int scores[value];
};

In addition, it's legal to take const address, illegal to take enum address, and illegal to take "define" address. From this point of view, enum is more like "define"

#define TIME 5
enum {TIME2 = 5};
const int TIME3 = 5;
static const int TIME4 = 5;

Summary:

  • For simple constants, replace ා define with const object or enums
  • For a function like macro, it's best to replace "define" with "inline" function

Item 3: use const as much as possible

const, as a kind of constraint, can distinguish whether the object can be changed or not, so as to prevent customers from using your code incorrectly.

Declaration of constants

Declaration of pointer:

char greeting[] = "Hello";
char* p = greeting;                 //non-const pointer, non-const data
const char* p = greeting;           //non-const pointer, const data
char* const p = greeting;           //const pointer, non-const data
const char* const p = greeting;     //const pointer, const data

When const appears on the left side of * the object pointed to is a constant, and when const appears on the right side of * the pointer itself is a constant.
const on the left and right sides of a type is equivalent

void f1(const Widget* pw);
void f2(Widget const * pw);

STL iterators are molded on the basis of pointers. The function of an iterator is like a T * pointer. If you want the data referred by an iterator to be immutable, you need const_iterator

std::vector<int> vec;
//iter like T* const
const std::vector<int>::iterator iter = vec.begin();    
*iter = 10;     //ok, change what iter point to
++iter;         //error, iter is const

//cIter like const T*
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10;    //error, *cIter is const
++cIter;        //ok, change cIter

For return values, declaring const also prevents accidents

//The product of two rational numbers does not need to accept an assignment
const Rational operator*(const Rational& lhs, const Rational& rhs);

//To avoid such violence, the compiler will prevent
Rational a, b, c;
if (a * b = c) {
}

The pointer, iterator, parameter and return value can be declared as const as much as possible

const member function

There is an obvious advantage of const implemented in member functions: it makes the class interface easy to understand and know which function can change the object and which can't.

Adding a constant qualifier to a member method is a function overload, so you need to add the const keyword to the implementation outside the class.
Constant objects can only call constant methods. Non constant objects call non constant methods first. If there is no non constant method, constant methods with the same name will be called.

class TextBlock {
public:
    const char& operator[](std::size_t position) const
    { return text[position]; }
    
    char& operator[](std::size_t position)
    { return text[position]; }
private:
    std::string text;
};

TextBlock tb("hello");
const TextBlock ctb("world");
std::cout << tb[0];         //ok read non-const
std::cout << ctb[0];        //ok read const
tb[0] = 'x';                //ok write non-const
ctb[0] = 'x';               //error wirte const

Bitwise const: a member function can only be said to be const if it does not change any member variables of the object (except for static). That is to say, const member function cannot modify non constant data members.
Bit constant is the definition of constancy in C + +. However, a method that satisfies bit constancy may not represent const. It does satisfy bitwise property, but its result is anti intuitive: it changes the value of pointer

class TextBlock{
    char* text;
public:
    char& operator[](int pos) const{
        return text[pos];
    }
};

const TextBlock tb;
char *p = &tb[1];
*p = 'a';

This kind of error should not exist, because creating a constant object and calling const member function on it only changes its value (although the bit of the object does not change, the referred value changes)
This led to:

  • Non const data members cannot be modified
  • The data referred to by the non const data pointer can be modified

So another view is derived. The const member function can modify some bits in the object it processes, but only when the client cannot detect them. This view is called logical constancy
The C + + compiler will not agree to modify the data member directly by constant member function. In this case, use mutable to release bitwise constness of non static member variable

class CTextBlock {
public:
  std::size_t length() const;
  
private:
  char *pText;

  mutable std::size_t textLength;         // these data members may
  mutable bool lengthIsValid;             // always be modified
};                                     

std::size_t CTextBlock::length() const{
  if (!lengthIsValid) {
    textLength = std::strlen(pText);
    lengthIsValid = true;          
  }
  return textLength;
}

Avoid duplication in const and non const member functions

Usually, we will define constant member functions and ordinary member functions in pairs. Maybe these two functions only have different return values. It is not what we want to write two identical logics. Const member function is not allowed to call non const member function, so it can only be reversed

const char& operator[](size_t pos) const{
    ...
}

char& operator[](size_t pos){
    return const_cast<char&>(
        static_cast<const TextBlock&>(*this)
            [pos]   
    );
}

We need to call the const method, so we need to convert ordinary objects to const objects, of course, using static_ cast<>
Return to remove the const attribute and call const_cast
Note that the return type is a reference, and the conversion of the type into what needs to be considered

Item 4: make sure the object is initialized before being used

Reading uninitialized variables can lead to ambiguous behavior, so it is necessary to initialize variables before using them. It is a good habit to initialize variables when constructing objects.

  • The C part of C + + does not guarantee content initialization of array, while the STL part can guarantee std::vector initialization.
  • It is necessary to understand the definition of default values, such as global variables, local variables uninitialized values.
  • The best way to do this is to initialize before using the object. For built-in types, initialize manually. For others, initialization depends on the constructor. Please initialize each variable in the constructor.
  • Constructor initialization is done in the initialization list, not inside the constructor.
  • const member variables and reference type member variables are initialized in the initialization list.
  • At the same time, you should know the initialization order to avoid unexpected results. The initialization order of a class is the same as that of a member declaration, rather than the initialization list order. The base class is always initialized before the derived class.

Best practice: replace non local static with local static

//file1.cc
extern int i = 1;

//file2.cc
int j = i + 1;

In fact, there is no guarantee which is initialized first. The method is to write it to the function and replace it with local static

class Ex
{
public:
    static int getI() {
        static int i = 2;
        return i;
    }
private:
};

//file2.cc
int j = Ex::getI() + 1;

This is similar to the implementation of the singleton pattern: lazy singleton -- local static variables
Note: this method is thread safe

class Singleton
{
public:
    ~Singleton() {}
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
    static Singleton& Singleton::getInstance()
    {
        static Singleton instance;
        return instance;
    }
private:
    Singleton() {}
};

Topics: Programming Attribute