Problem Aspects of C ++ Programming


    In C ++ there are a lot of features that can be considered potentially dangerous - when they are miscalculated in design or inaccurate coding, they can easily lead to errors. The article provides a selection of such features, given tips on how to reduce their negative impact.




    Table of contents


    Table of contents

        Введение
        1. Типы
            1.1. Условные инструкции и операторы
            1.2. Неявные преобразования типа (implicit conversions)
        2. Разрешение имен
            2.1. Сокрытие переменных во вложенных областях видимости
            2.2. Перегрузка функций
        3. Конструкторы, деструкторы, инициализация, удаление
            3.1. Функции-члены класса, генерируемые компилятором
            3.2. Неинициализированные переменные
            3.3. Порядок инициализации базовых классов и нестатических членов класса
            3.4. Порядок инициализации статических членов класса и глобальных переменных
            3.5. Исключения в деструкторах
            3.6. Удаление динамических объектов и массивов
            3.7. Удаление при неполном объявлении класса
        4. Операторы, выражения
            4.1. Приоритет операторов
            4.2. Перегрузка операторов
            4.3. Порядок вычисления подвыражений
        5. Виртуальные функции
            5.1 Переопределение виртуальных функций
            5.2 Перегрузка и использование параметров по умолчанию
            5.3 Вызов виртуальных функций в конструкторе и деструкторе
            5.4 Виртуальный деструктор
        6. Непосредственная работа с памятью
            6.1 Выход за границу буфера
            6.2 Z-terminated строки
            6.3 Функции с переменным числом параметров
        7. Синтаксис
            7.1 Сложные объявления
            7.2 Неоднозначность синтаксиса
        8. Разное
            8.1 Ключевое слово inline и ODR
            8.2 Заголовочные файлы
            8.3 Инструкция switch
            8.4 Передача параметров по значению
            8.5 Управление ресурсами
            8.6 Владеющие и невладеющие ссылки
            8.7 Двоичная совместимость
            8.8 Макросы
        9. Итоги
        Список литературы



                            Praemonitus, praemunitus.
                            Forewarned is forearmed. (lat.)



    Introduction


    In C ++ there are a lot of features that can be considered potentially dangerous - when they are miscalculated in design or inaccurate coding, they can easily lead to errors. Some of them can be attributed to difficult childhood, some to the obsolete C ++ 98 standard, but others are already related to the features of modern C ++. Consider the main ones and try to give advice on how to reduce their negative impact.



    1. Types



    1.1. Conditional instructions and operators


    The need for compatibility with C leads to the fact that if(...)any numeric expression or pointer, and not just type expressions, can be substituted into instructions and similar bool. The problem is aggravated by the implicit conversion of from boolto intin arithmetic expressions and the priority of some operators. This leads, for example, to such errors:


    if(a=b)As well if(a==b),
    if(a<x<b)as well if(a<x && x<b),
    if(a&x==0)as well if((a&x)==0),
    if(Foo)as well if(Foo()),
    if(arr)as well if(arr[0]),
    if(strcmp(s,r))when properly if(strcmp(s,r)==0).


    Some of these errors cause a compiler warning, but not an error. Also, code analyzers can sometimes help. In C #, such errors are almost impossible, instructions if(...)and similar require type bool, booland numeric types in arithmetic expressions cannot be mixed .


    How to fight:


    • Program without warnings. Unfortunately, this does not always help, some of the errors described above do not give warnings.
    • Use static code analyzers.
    • Old reception: when compared with a constant, put it on the left, for example if(MAX_PATH==x). It looks pretty kondovo (and even has its own name - “Yoda notation”), and it helps in a small number of the cases considered.
    • Use qualifier as widely as possible const. Again, it does not always help.
    • Train yourself to write correct logical expressions: if(x!=0)instead if(x). (Although it is possible to fall into the trap of operator priorities, see the third example.)
    • Be extremely attentive.


    1.2. Implicit conversions of type (implicit conversions)


    C ++ refers to languages ​​with strong typing, but implicit type conversions are quite widely used to make the code shorter. These implicit conversions can in some cases lead to errors.


    Most unpleasant implicit conversions - a conversion of a numeric type, or a pointer to booland from boolto int. It is these transformations (necessary for compatibility with C) that cause the problems described in section 1.1. Also not always appropriate implicit conversions, potentially causing a loss of accuracy of numeric data (narrowing conversion), for example doubleto int. In many cases, the compiler issues a warning (especially when there may be a loss of precision in numeric data), but a warning is not an error. In C #, conversions between numeric types and are boolforbidden (even explicit), and conversions that potentially cause a loss of precision of numeric data are almost always an error.


    The programmer may add other implicit conversions: (1) by defining a constructor with one parameter without a keyword explicit; (2) the definition of a type conversion operator. These transformations make additional security gaps based on strong typing.


    In C #, the number of built-in implicit conversions is significantly less; custom implicit conversions must be declared using a keyword implicit.


    How to fight:


    • Program without warnings.
    • Be very careful about the structures described above, do not use them without extreme need.


    2. Name resolution



    2.1. Hiding variables in nested scopes


    In C ++, the following rule applies. Let be


    // Блок А
    {
        int x;
        // ...// Блок Б, вложен в А
        {
            int x;
            // ...
        }
    }

    According to the rules of C ++, a variable хdeclared in Бhides (hide) the variable хdeclared in А. The first declaration xdoes not have to be in a block: it can be a member of a class or a global variable, it should just be visible in a block Б.


    Imagine now the situation when you need to refactor the following code.


    // Блок А
    {
        int x;
        // ...// Блок Б, вложен в А
        {
        // что-то делается с х из А
        }
    }

    By mistake, changes are made:


    // Блок Б
    {
        // новый код, ошибочный:
        int x;
        // что-то делается с х из Б
        // ...
        // старый код:
        // что-то делается с х из А
    }

    But now the code “something is done with хof А” will have something to do with хof Б! It is clear that everything does not work the way it used to, and it’s often hard to find what's the matter. Not for nothing in C # to hide local variables is prohibited (although members of the class can). Note that the mechanism for hiding variables in one way or another is used in almost all programming languages.


    How to fight:


    • Declare variables in the narrowest possible scope.
    • Do not write long and deep nested blocks.
    • Use coding conventions to visually distinguish between identifiers of different scopes.
    • Be extremely attentive.


    2.2. Function overload


    Function overloading is an integral feature of many programming languages, and C ++ is no exception. But this opportunity should be used deliberately, otherwise you can run into trouble. In some cases, for example, when the constructor is overloaded, the programmer has no choice, but in other cases, refusing the overload can be quite justified. Consider the problems that arise when using overloaded functions.


    If you try to consider all the possible options that may arise during overload resolution, the overload resolution rules turn out to be very complex, and therefore difficult to predict. Additional complexity is introduced by template functions and overloading of built-in operators. C ++ 11 added problems with rvalue links and initialization lists.


    Problems can create an algorithm for finding candidates for overload resolution in nested scopes. If the compiler has found some candidates in the current scope, then the further search is terminated. If the found candidates are not suitable, conflicting, deleted or inaccessible, an error is generated, but no further search is attempted. And only if there are no candidates in the current scope, the search moves to the next, wider scope. The name hiding mechanism works, almost the same as that discussed in section 2.1, see [Dewhurst].


    Overloading functions can reduce the readability of the code, and thus cause errors.


    Using functions with default parameters looks similar to using overloaded functions, although, of course, there are fewer potential problems. But the problem with the deterioration of readability and possible errors remains.


    Be especially careful to use overload and default parameters for virtual functions, see section 5.2.


    C # also supports function overloading, but the rules for overload resolution are slightly different.


    How to fight:


    • Do not abuse the overloading of functions, as well as the design of functions with default parameters.
    • If the functions are overloaded, then use signatures that are not in doubt when overload is resolved.
    • Do not declare functions of the same name in nested scopes.
    • Do not forget that the mechanism of remote functions ( =delete) , which appeared in C ++ 11 , can be used to prohibit certain overload options.


    3. Constructors, destructors, initialization, removal



    3.1. Compiler generated member functions


    If the programmer has not defined the class member functions from the following list — the default constructor, the copy constructor, the copy assignment operator, the destructor — then the compiler can do it for him. C ++ 11 added a relocation constructor and a move assignment operator to this list. These member functions are called special member functions. They are generated only if they are used, and additional conditions specific to each function are fulfilled. Note that this usage may be quite hidden (for example, when implementing inheritance). If the requested function cannot be generated, an error is generated. (With the exception of moving operations, they are replaced by copying ones.) The member functions generated by the compiler are public and embeddable.


    In some cases, such help from the compiler may turn out to be a “disservice”. The absence of custom special member functions can lead to the creation of a trivial type, and this, in turn, causes the problem of uninitialized variables, see section 3.2. The generated member functions are public, and this is not always consistent with the design of the classes. In base classes, the constructor must be protected; sometimes, for a more subtle control over the life cycle of an object, a protected destructor is needed. If a class has a raw resource descriptor as a member and owns that resource, then the programmer must implement the copy constructor, copy assignment operator, and destructor. The so-called “Big Three Rule” is well known, which claims that if a programmer has defined at least one of the three operations — a copy constructor, a copy assignment operator, or a destructor — then he must define all three operations. The displacement constructor and the displacement assignment operator generated by the compiler are also far from always what is needed. The destructor generated by the compiler in some cases leads to very subtle problems, which may result in resource leaks, see section 3.7.


    The programmer can prohibit the generation of special member functions, in C ++ 11, you need to apply a construction to the declaration "=delete", in C ++ 98, declare the corresponding member function closed and not define it.


    If the programmer is satisfied with the member functions generated by the compiler, then in C ++ 11 he can designate this explicitly, rather than simply dropping the declaration. To do this, you need to use a construction when declaring "=default", the code is better readable, and additional features appear associated with managing the access level.


    In C #, the compiler can generate a default constructor, usually it does not cause any problems.


    How to fight:


    • Control the generation of special member functions by the compiler. If necessary, implement them yourself or prohibit.


    3.2. Uninitialized variables


    Constructors and destructors can be called key elements of the C ++ object model. When an object is created, the constructor is invoked, and when it is deleted, the destructor is called. But compatibility issues with C forced to make some exceptions, and this exception is called trivial types. They are introduced to model the sish types and the life cycle of variables, without necessarily calling the constructor and destructor. Critical code, if it is compiled and executed in C ++, should also work as in C. Trivial types include numeric types, pointers, enums, and also classes, structures, unions, and arrays consisting of trivial types. Classes and structures must satisfy some additional conditions: the absence of a custom constructor, destructor, copy, virtual functions. For a trivial class, the compiler can generate a default constructor and destructor. The default constructor resets the object, the destructor does nothing. But this constructor will be generated and used, only if it is explicitly called when the variable is initialized. A variable of the trivial type will be uninitialized if you do not use any version of explicit initialization. The initialization syntax depends on the type and context of the declaration of the variable. Static and local variables are initialized upon declaration. For a class, immediate base classes and non-static class members are initialized in the constructor initialization list. (C ++ 11 allows to initialize non-static class members when declaring, see below.) For dynamic objects, the expression The default constructor resets the object, the destructor does nothing. But this constructor will be generated and used, only if it is explicitly called when the variable is initialized. A variable of the trivial type will be uninitialized if you do not use any version of explicit initialization. The initialization syntax depends on the type and context of the declaration of the variable. Static and local variables are initialized upon declaration. For a class, immediate base classes and non-static class members are initialized in the constructor initialization list. (C ++ 11 allows to initialize non-static class members when declaring, see below.) For dynamic objects, the expression The default constructor resets the object, the destructor does nothing. But this constructor will be generated and used, only if it is explicitly called when the variable is initialized. A variable of the trivial type will be uninitialized if you do not use any version of explicit initialization. The initialization syntax depends on the type and context of the declaration of the variable. Static and local variables are initialized upon declaration. For a class, immediate base classes and non-static class members are initialized in the constructor initialization list. (C ++ 11 allows to initialize non-static class members when declaring, see below.) For dynamic objects, the expression if it is explicitly called when the variable is initialized. A variable of the trivial type will be uninitialized if you do not use any version of explicit initialization. The initialization syntax depends on the type and context of the declaration of the variable. Static and local variables are initialized upon declaration. For a class, immediate base classes and non-static class members are initialized in the constructor initialization list. (C ++ 11 allows to initialize non-static class members when declaring, see below.) For dynamic objects, the expression if it is explicitly called when the variable is initialized. A variable of the trivial type will be uninitialized if you do not use any version of explicit initialization. The initialization syntax depends on the type and context of the declaration of the variable. Static and local variables are initialized upon declaration. For a class, immediate base classes and non-static class members are initialized in the constructor initialization list. (C ++ 11 allows to initialize non-static class members when declaring, see below.) For dynamic objects, the expression For a class, immediate base classes and non-static class members are initialized in the constructor initialization list. (C ++ 11 allows to initialize non-static class members when declaring, see below.) For dynamic objects, the expression For a class, immediate base classes and non-static class members are initialized in the constructor initialization list. (C ++ 11 allows to initialize non-static class members when declaring, see below.) For dynamic objects, the expressionnew T()creates an object initialized by the default constructor, but new Tfor trivial types it creates an uninitialized object. When creating a dynamic array of the trivial type,, new T[N]its elements will always be uninitialized. If an instance is created or expanded std::vector<T>and no parameters are provided for explicitly initializing elements, then they are guaranteed to invoke the default constructor. In C ++ 11, a new initialization syntax has appeared - using curly braces. An empty pair of parentheses means initialization using the default constructor. Such initialization is possible everywhere where traditional initialization is used, besides it became possible to initialize non-static members of the class when declaring, which replaces initialization in the initialization list of the constructor.


    An uninitialized variable is organized as follows: if it is defined in scope namespace(globally), it will have all the bits zero, if it is local, or created dynamically, then it will receive a random set of bits. It is clear that the use of such a variable can lead to unpredictable program behavior.


    True progress does not stand still, modern compilers, in some cases, detect uninitialized variables and give an error. Uninitialized variable code analyzers are even better detected.


    In the standard library C ++ 11 there are templates called type properties (header file <type_traits>). One of them allows you to determine whether a type is trivial. The expression std::is_trivial<Т>::valuematters trueif the Ttype is trivial and falseotherwise.


    Cinnamon structures are also often referred to as Plain Old Data (POD). We can assume that POD and "trivial type" are practically equivalent terms.


    In C #, uninitialized variables cause an error, this is controlled by the compiler. Fields of objects of a reference type are initialized by default if explicit initialization is not performed. Fields of objects of a meaningful type are initialized either by default or all must be explicitly initialized.


    How to fight:


    • Have a habit of explicitly initializing a variable. An uninitialized variable should "cut the eye."
    • Declare variables in the narrowest possible scope.
    • Use static code analyzers.
    • Do not design trivial types. In order for the type not to be trivial, it suffices to define a custom constructor.


    3.3. The order of initialization of base classes and non-static class members


    When implementing a class constructor, immediate base classes and non-static class members are initialized. The initialization order defines the standard: first, the base classes in the order in which they are declared in the list of base classes, then the non-static members of the class in the order of declaration. If necessary, explicit initialization of base classes and non-static members is used to list the constructor initialization. Unfortunately, the elements of this list are not required to be in the order in which initialization occurs. This should be taken into account if during initialization the elements of the list use references to other elements of the list. If an error occurs, the link may be to an object that has not yet been initialized. C ++ 11 allows you to initialize non-static class members when they are declared (using curly braces).


    In C #, an object is initialized as follows: first, the fields are initialized, from the base sub-object to the last derivative, then the constructors are called in the same order. The described problem does not occur.


    How to fight:


    • Maintain constructor initialization list in order of declaration.
    • Try to make the initialization of base classes and class members independent.
    • Use non-static member initialization when declaring.


    3.4. Initialization order for static class members and global variables


    Static class members, as well as variables defined in scope namespace(globally) in different compilation units (files), are initialized in the order defined by the implementation. This must be taken into account if, during initialization, such variables use references to each other. The link may be on a variable that has not yet been initialized.


    How to fight:


    • Take special measures to prevent such a situation. For example, to use local static variables (singletons), they are initialized at the first use.


    3.5. Destructor exceptions


    The destructor should not throw exceptions. If you violate this rule, you can get an undefined behavior, most often crash.


    How to fight:


    • Do not allow an exception to be thrown in the destructor.


    3.6. Removing dynamic objects and arrays


    If you create a dynamic object of some type T


    T* pt = new T(/* ... */);

    then it is deleted by the operator delete


    delete pt;

    If you create a dynamic array


    T* pt = new T[N];

    then it is deleted by the operator delete[]


    delete[] pt;

    If you do not follow this rule, you can get undefined behavior, that is, anything can happen: a memory leak, crash, etc. See [Meyers1] for details.


    How to fight:


    • Use the correct form delete.


    3.7. Deletion on incomplete class declaration


    Certain problems can be created by the operator's “omnivorous nature” delete; it can be applied to a type pointer void*or to a pointer to a class that has an incomplete (proactive) declaration. The operator deleteapplied to the pointer to the class is a two-phase operation, first the destructor is called, then the memory is released. If the operator is applied deleteto a pointer to a class with an incomplete error declaration, the compiler simply skips the destructor call (although a warning is issued). Consider an example:


    classX;// неполное объявлениеX* CreateX();
    voidFoo(){
         X* p = CreateX();
         delete p;
    }

    This code is compiled, even if the deletefull class declaration is not available at the call point X. Visual Studio gives the following warning:

    warning C4150: deletion of pointer to incomplete type 'X'; no destructor called


    If there is an implementation Xand CreateX(), then the code is linked, if it CreateX()returns a pointer to the object created by the operator new, the call Foo()succeeds, and the destructor is not called. It is clear that this can lead to a drain on resources, so once again the need to carefully consider warnings.


    This situation is not contrived, it can arise when using classes such as the smart pointer or descriptor classes. The occurrence of such a situation can stimulate the destructor generated by the compiler. Standard smart pointers are protected from such an error, so the compiler will give an error message, but homemade classes, such as the smart pointer, may be limited to a warning. Scott Meyers deals with this problem in [Meyers2].


    How to fight:


    • Program without warnings.
    • Declare the destructor explicitly and define it in the scope of a full class declaration.
    • Use validation at compile time.


    4. Operators, expressions



    4.1. Operators priority


    There are many operators in C ++, their priority is not always obvious. Do not forget about associativity. And the compiler does not always detect a similar error. The situation is aggravated by the problems described in section 1.1.


    Let's give an example:


    std::сout<<c?x:y;

    This is actually a rather pointless instruction.


    (std::сout<<c)?x:y;

    but not


    std::сout<<(c?x:y);

    as the programmer most likely expects.


    All instructions above are compiled without errors and warnings. The problem in this example lies in the unexpectedly higher operator priority <<compared to operator priority ?:and the presence of an implicit conversion from std::сoutto void*. In C ++, there is no special operator for writing data to the stream and you have to resort to overloading, in which the priority does not change. In a good way, the priority of the operator of writing data to the stream should be very low, at the level of the assignment operator. Low priority operator ?:can create problems in other cases. In fact, it must be enclosed in brackets whenever it is a subexpression (except for the simplest assignment).


    Here is another example: the expression is x&f==0in fact x&(f==0), and not (x&f)==0, as the programmer most likely expects. For some reason, operators of bitwise operations have a low priority, although, from the point of view of common sense, they should be in priority in the group of arithmetic operators, before comparison operators.


    Another example. Multiplication / division of integers by a power of two can be replaced by a bitwise shift. But multiplication / division have a higher priority than addition / subtraction, and the shift is lower. Therefore, if we replace the expression x/4+1with x>>2+1, then we get x>>(2+1), not (x>>2)+1, as needed.


    C # has almost the same set of operators as C ++, with the same priorities and associativity, but fewer problems due to stricter typing and overload rules.


    How to fight:


    • Do not spare brackets, put them at the slightest doubt. This, by the way, often improves readability of the code.


    4.2. Operator Overloading


    C ++ allows you to overload almost all operators, but you need to use this feature carefully. The meaning of the overloaded operator should be obvious to the user. Do not forget about the priority and associativity of operators, they do not change when overloaded and must meet the user's expectations, see section 4.1. A good example of overloading is the use of operators +and +=for string concatenation. Some operators are not recommended to overload. For example, the following three statements: ,(comma) &&,||. The fact is that for them the standard provides for the order of calculating operands (from left to right), and for the last two it also provides the so-called semantics of fast calculations (short circuit analysis semantics), but for overloaded operators this is no longer guaranteed, which can be quite unpleasant surprise for the programmer. It is also not recommended to overload the & operator (taking the address). Type with overloaded operator & it is dangerous to use with templates, since they can use the standard semantics of this operator.


    Almost all operators can be overloaded in two versions, as a member operator and as a free (non-member) operator, and at the same time. This style can make life difficult.


    If overloading is done after all, a number of rules must be followed, depending on the operator being overloaded. See [Dewhurst] for details.


    C # also supports operator overloading, but the overload rules are stricter, so there are fewer potential problems.


    How to fight:


    • Carefully consider operator overloading.
    • Do not overload operators that are not recommended for reloading.


    4.3. The procedure for calculating subexpressions


    In general, the C ++ standard does not define the order of evaluation of subexpressions in a complex expression, including the order of evaluation of arguments when calling a function. (The exceptions are four operators: ,(comma), &&, ||, ?:.) This can lead to the fact that the expressions, compiled by different compilers, will have different values. Here is an example of such an expression:


    int x=0;
    int y=(++x*2)+(++x*3);

    The value ydepends on the order of calculation of the increments.


    If an exception is thrown during the calculation of a subexpression, then under adverse conditions a resource leak may occur. Here is an example.

    classX;classY;voidFoo(std::shared_ptr<X>, std::shared_ptr<Y>);

    Let Foo()it be called as follows:


    Foo(std::shared_ptr<X>(new X()), std::shared_ptr<Y>(new Y()));

    Let the arguments be calculated as follows: constructor X, constructor Y, constructor std::shared_ptr<X>, constructor std::shared_ptr<Y>. If the constructor Ythrows an exception, the instance Xwill not be deleted.


    The correct code can be written like this:


    auto p1 = std::shared_ptr<X>(new X());
    auto p2 = std::shared_ptr<Y>(new Y());
    Foo(p1, p2);

    It is even better to use a template std::make_shared<Y>(but it has limitations, it does not support custom deleters):


    Foo(std::make_shared<X>(), std::make_shared<Y>());

    See [Meyers2] for details.


    How to fight:


    • Think through the construction of complex expressions.


    5. Virtual functions



    5.1. Redefinition of virtual functions


    In C ++ 98, a redefinition is made if the function in the derived class coincides with the virtual one by name (except for the destructor), parameters, constancy and return value (there is some relief to the return value, called covariant return values). An additional confusion is introduced by the keyword virtual, it can be used, or it can be omitted. In case of an error (an elementary typo), the redefinition does not occur, sometimes a warning is issued, but often it happens silently. Naturally, the programmer does not get what he intended. Fortunately, in C ++ 11 a keyword appeared overridethat makes life much easier, the compiler detects all errors, and the readability of the code is noticeably improved. But the old style of overriding virtual functions is left for backward compatibility.


    How to fight:


    • Use keyword override.
    • Use pure virtual functions. If this is not overridden, then the compiler detects this when trying to create an instance of a class.


    5.2. Overloading and using default parameters


    You should be very careful about using overload and default parameters for virtual functions. The fact is that overload resolution and the default parameter update are done based on the static type of the variable for which the virtual function is called. This is inconsistent with the dynamic nature of virtual functions and can lead to rather unexpected results. Details and examples see [Dewhurst].


    How to fight:


    • Very carefully use overload and default parameters for virtual functions.


    5.3. Calling virtual functions in the constructor and destructor


    Sometimes, when designing a polymorphic hierarchy of classes, there is a need to perform a polymorphic operation when creating or destroying an object. For example, operations that can be called post_construct or pre_destroy. The first thing that comes to mind is to insert a call to a virtual function in a constructor or destructor. But it will be a mistake. The fact is that polymorphism does not work in the constructor and destructor: the function is always overridden (or inherited) for the corresponding class. (And, accordingly, this function may turn out to be purely virtual.) If this were not done, then the virtual function would be called for an object that has not yet been created (in the constructor) or already destroyed (in the destructor). See [Dewhurst] for details. Note that a virtual function call can be hidden inside another,


    One solution to this problem is to use the factory function to create an object and a special virtual function to delete.


    Interestingly, in C #, the virtual function called in the constructor of the base class is a virtual function redefined at the end of the inheritance chain. In C #, an object is initialized as follows: first, the fields are initialized, from the base sub-object to the last derivative, then the constructors are called in the same order. Thus, such a virtual function can be called for a partially initialized object (the fields were initialized, and the constructor was not called).


    How to fight:


    • Do not call virtual functions in the constructor and destructor, including indirectly, through another function.


    5.4. Virtual destructor


    If a polymorphic class hierarchy is being designed, then there must be a virtual destructor in the base class, this guarantees that the actual type of the object will be called when the operator is applied deleteto the pointer to the base class. If this rule is violated, a call to the base class destructor may occur, due to which resource leakage is possible.


    How to fight:


    • Declare the base class destructor virtual.


    6. Direct work with memory


    The ability to directly work with memory through pointers is one of the key features of C / C ++, but it is also one of the most dangerous. Unobtrusive error and the code starts to work beyond the limits of its allocated memory. The most destructive consequences cause such errors when writing. People call them “memory shooting”.


    In C #, direct work with memory is possible only in unsafe mode, which is disabled by default.



    6.1. Overrun buffer


    The standard library of C / C ++, many functions that can record data for the border of the destination buffer: strcpy(), strcat(), sprinf(), etc. Containers of the standard library ( std::vector<>, etc.) in some cases do not control the overflow of the buffer reserved for data storage. (However, it is possible to work with the so-called debug version of the standard library, where tighter control is exercised over access to data, naturally by reducing efficiency. See Checked Iterators in MSDN.) Such errors can sometimes go unnoticed, but they can also give unpredictable results. : if the buffer is a stack, then anything can happen, for example, the program can silently disappear; if the buffer is dynamic or global, then a memory protection error may occur.


    In C #, if unsafe mode is disabled, no memory access errors are guaranteed.


    How to fight:


    • To use an object variant of a line, a vector.
    • Use the debug version of standard containers.
    • Use safe functions for z-terminated strings, they have a suffix _s(see the corresponding compiler warnings).


    6.2. Z-terminated strings


    If a terminal zero is lost in such a string, then trouble. And you can lose it, like this:


    strncpy(dst,src,n);

    If strlen(src)>=n, then it dstwill be without a terminal zero (of course, if you do not take care of this additionally). Even if the terminal zero is not lost, it is easy to write data beyond the boundary of the target buffer, see the previous section. Do not forget about the problem of efficiency - the search for terminal zero is done by scanning the entire line. Obviously more effective if(*str)than if(strlen(str)>0), and with a large number of long lines, the difference can be very significant. Read the parable about the painter Slamile by Joel Spolsky [Spolsky].


    In C #, the type stringworks absolutely reliably and as efficiently as possible.


    How to fight:


    • Use object variant string.
    • To use safe functions for working with z-terminated strings, they have a suffix _s(see the corresponding compiler warnings).


    6.3. Functions with a variable number of parameters


    Such functions are ...at the end of the list of parameters. The most well-known family of so-called printf-like functions included in the standard C library. In this case, the programmer must carefully control the type of arguments, more often than usual, you have to do explicit type conversions, the compiler does not help in this case. When an error occurs, a memory access violation occurs quite often, although sometimes it may just turn out to be an incorrect result.


    In C #, there are similar printffunctions, but they work more reliably.


    How to fight:


    • Avoid such features whenever possible. For example, instead of-like printffunctions use I / O streams.
    • Be extremely attentive.


    7. Syntax



    7.1. Sophisticated ads


    C ++ has a rather peculiar syntax for declaring pointers, references, arrays, and functions, making it very easy to write quite inconvenient declarations. Here is an example:


    constint N = 4, M = 6;
    int x,                 // 1
        *px,               // 2
        ax[N],             // 3
        *apx[N],           // 4
        F(char),           // 5
        *G(char),          // 6
        (*pF)(char),       // 7
        (*apF[N])(char),   // 8
        (*pax)[N],         // 9
        (*apax[M])[N],     // 10
        (*H(char))(long);  // 11

    In Russian, these variables can be described as:


    1. type variable int;
    2. pointer to int;
    3. array of the size Nof type elements int;
    4. array of size Nelements of type pointer to int;
    5. the function that receives charand returns int;
    6. a function that takes charand returns a pointer to int;
    7. pointer to the function that receives charand returns int;
    8. An array of the size of the Nelements of the type pointer to the function that receives charand returns int;
    9. pointer to an array of the size of Nelements of the type int;
    10. array of size Mof type Nelements int; pointer to array of size of type elements ;
    11. a function that receives charand returns a pointer to a function that receives longand returns int.

    Recall that a function cannot return a function or an array, and you cannot declare an array of functions. (It would be even worse.)


    In many examples, the symbol *can be replaced by &and then you get the link declaration. (But you cannot declare an array of links.)


    Such announcements can be simplified using intermediate typedef(or- usingaliases). For example, the last declaration can be rewritten as:


    typedefint(*P)(long);
    P H(char);

    A certain skill of decoding such announcements is needed, but you should not abuse it.


    C # has a slightly different syntax for declarations, such examples are not possible.


    How to fight:


    • Use intermediate aliases.


    7.2. Syntax ambiguity


    In some cases, the compiler cannot unambiguously determine the semantics of some instructions. Let there be some class


    classX
    {public:
        X(int val = 0);
    // ...
    };

    In this case, the instruction


    X x(5);

    is a definition of a xtype variable Xinitialized to value 5. And here is the instruction


    X x();

    is a declaration of a function xthat returns a type value Xand does not accept parameters, and not a xtype variable definition Xinitialized with a default value. To define a type variable Xinitialized with a default value, you must select one of the options:


    X x;
    X x = X();
    X x{};    // только в C++11

    This is an old problem, and even then it was decided that if a structure can be interpreted as a definition and as an announcement, then an announcement is selected. More complex examples of the problem described can be found in [Sutter].


    Let's pay attention to the fact that in C ++ functions can be declared locally (although this style is difficult to attribute to common ones). The definition of such functions should be in the global scope. (Local function definitions are not supported in C ++.)


    Such errors, of course, can not lead to serious consequences, the compiler will surely detect an error in the following code, but error messages can lead to some confusion.


    In C #, there is no such problem, functions can only be declared in the scope of a class, and the syntax for defining variables is somewhat different.


    How to fight:


    • Remember about this problem.


    8. Miscellaneous



    8.1. Keyword inlineand ODR


    Many programmers believe that a keyword inlineis a request to the compiler to embed, if possible, the function body directly at the call point. But it turns out that this is not all. The keyword inlineaffects the implementation of the compiler and linker rules for one definition (One Defenition Rule, ODR). Consider an example. Suppose that in two files there are two functions with the same name and signature, but different bodies. When compiling and linking, the linker will generate an error about duplicating a symbol, ODR works. Add a keyword to the definition of functions static: there will be no error now, each file will use its own version of the function, local linking works. Now replace staticoninline. Compilation and linking pass without error, but both files will use the same version of the function, ODR works, but in a different version. It is clear that this can be a very unpleasant surprise. The member functions of the class that are defined directly upon class declaration and the template functions and member functions are processed in the same way. But in this case the probability of a similar problem is much less.


    How to fight:


    • Avoid "bare" inlinefunctions. Try to identify them in the class or at least namespace. This does not guarantee the absence of such an error, but significantly reduces its probability.
    • Use local binding or more advanced technique - anonymous namespace.


    8.2. Header files


    Unreasonable use of header files can cause many problems. All parts of the project become dependent on each other. Header files go where they are not needed, clog up with unused scope names, create unnecessary conflicts, increase compile time and code size.


    How to fight:


    • Carefully think through the code of the header files, especially the inclusion of other header files.
    • Use a technique that reduces dependency on header files: incomplete (anticipatory) declarations, interface classes, and descriptor classes.
    • Never include in the header file or before other header files the usingdirective:, as well as the declarations.using namespace имяusing
    • Carefully use functions and variables with local binding in header files.


    8.3. Instructionswitch


    A typical mistake is the absence of breaka branch at the end case. (This is called failing.) In C #, such errors are detected during compilation.


    How to fight:


    • Be extremely attentive.


    8.4. Passing parameters by value


    In C ++, the programmer must decide for himself how to implement the transfer of function parameters — by reference or by value — the language and the compiler do not provide any help. Custom type objects (declared as classor struct) are usually passed by reference, but it is easy to make a mistake and the transfer will be implemented by value. (Such errors may occur more often in those who are accustomed to the C # or Java programming style.) Passing by value is copying an argument that can cause the following problems.


    1. Copying an object is almost always less effective than copying a link. If the type of the parameter owns the resource and uses the deep copying strategy (and this std::string, std::vectoretc.), then the resource is copied, which is usually not needed at all and leads to an additional decrease in efficiency.
    2. If the function changes the object, then these changes will be made with a local copy, the calling context will not see it.
    3. If the argument has a derived type with respect to the type of the parameter, then so-called slicing occurs, all information about the derived type is lost, and there is no need to talk about any polymorphism.

    If an object should be changed by a function, then the parameter should be passed by reference, if not, then by reference to a constant. An exception must always be intercepted by reference. In general, solutions with parameter passing by value are possible, but are rarely required and must be carefully justified. For example, in the standard library, iterators and functional objects are passed by value. When designing a class, you can prohibit the transfer of instances of this class by value. A more crude way is to declare the copy constructor as deleted ( =delete), a more subtle one is to declare the copy constructor as explicit.


    In C #, the parameters of the reference type are passed by reference, but for parameters of the significant type, the programmer must control the type of transfer.


    How to fight:


    • Be extremely attentive, implement the correct type of parameter transfer.
    • If necessary, prohibit the transfer of parameters by value.


    8.5. Resource management


    In C ++ there are no tools for automatic resource management such as garbage collection. The programmer must decide for himself how to free up unused resources. Object-oriented features of the language allow you to implement the necessary tools (and often not in one way), the standard C ++ 11 library has intelligent pointers, but the programmer can still manage resources manually, in a shared style, and here the only salvation from resource leaks is attention and accuracy.


    How to properly manage resources in C ++ is described in detail here .


    C # has a garbage collector that solves a significant portion of resource management problems. True, the garbage collector is not very suitable for resources, such as OS kernel objects. In this case, manual or semi-automatic (using-block) resource management is used based on the Basic Dispose pattern.


    How to fight:


    • Use object-oriented tools such as smart pointers to manage resources.


    8.6. Owning and not knowing links


    In this section, the term "link" will be understood in a broad sense. It can be a raw pointer, a smart pointer, a C ++ reference, an STL iterator, or something else.


    Links can be divided into owning and not owning. Owning links guarantee the presence of the object they refer to. An object cannot be deleted while at least one link to it is available. Owned links do not provide such a guarantee. A non-proprietary link can at any moment become “hanging”, that is, refer to a remote object. As an example of owning references, you can use pointers to COM interfaces and smart pointers of the standard library. (Of course, if you use them correctly.) But in spite of a certain danger, non-possessive references in C ++ are used quite widely. And one of the main examples is the containers and iterators of the standard library. The standard library iterator is a typical example of not owning links. The container can be deleted and the iterator will not know anything about it. Moreover, an iterator may become invalid (“hanging” or pointing to another element) during the lifetime of the container, as a result of a change in its internal structure. But programmers have been working with this for decades.


    In C #, almost all links are owned, the garbage collector is responsible for this. One of the few exceptions is the result of the delegate marshaling.


    How to fight:


    • Use owning links.
    • When using non-proprietary links, be careful and careful.


    8.7. Binary compatibility


    The C ++ standard regulates very little the internal structure of objects, as well as other aspects of the implementation: the function call mechanism, the virtual function table format, the implementation of the exception mechanism. (Even the size of the built-in types is not fixed!) All this is determined by the platform and the compiler. The interaction of modules is most often carried out through header files that are compiled separately in each module. Naturally, there are compatibility issues for modules compiled by different compilers. Even modules compiled by one compiler, but with different compilation keys, may be incompatible. (For example, the offset of the structure members may be different for different values ​​of the alignment parameter.)


    C (but still not complete) is somewhat more binary compatible, so for C ++ modules, C functions (declared in a block extern "C") are often used as an interface . Such functions are equally treated by all C / C ++ compilers.


    To solve the problem of uniform alignment of members of structures, sometimes stub members are added. You can use the #pragmacompiler directives to manage the alignment, but they are not standardized, depend on the compiler.


    The exception mechanism also implies a high degree of module compatibility, therefore, if it cannot be ensured, return codes must be used.


    They tried to solve the problem of binary compatibility, for example, in the development of COM standards. COM objects used in different modules are binary compatible (even if written in different languages, not to mention different compilers). But COM is not a very popular technology, moreover, not implemented on all platforms.


    There are almost no binary compatibility issues in C #. Probably, the only exception is the result of the marshaling of objects, but this is probably not a C #, but an aspect of the interaction between C # and C / C ++.


    How to fight:


    • Know about this problem and make adequate decisions.


    8.8. Macros


    Macros require double care and accuracy when writing, the problems here are an order of magnitude more due to the primitiveness of the preprocessor. The code becomes difficult to read, and potential errors can be extremely difficult to detect. In C ++, there are quite a few alternatives to macros. Instead


    #define XXL 32

    can write


    constint XXL=32;

    or use an enumeration. Instead of macros with parameters, you can define inlinefunctions and templates.


    There are no macros in C # (except for conditional compilation directives).


    How to fight:


    • Do not use macros without the most extreme need.


    9. Results


    1. Use the features of the compiler to prevent errors. Configure the compiler to issue the maximum number of warnings. Program without warning. If you have a few dozen warnings, then notice really dangerous is not very easy.
    2. Use static code analyzers.
    3. Do not use sishnye archaisms. Program in C ++ and preferably on the modern version - C ++ 11/14/17.
    4. Program in object-oriented style, use object-oriented libraries and templates.
    5. Do not use too complex and dubious language constructs.


    Bibliography


    List

    [Dewhurst]
    Дьюхэрст, Стефан К. Скользкие места C++. Как избежать проблем при проектировании и компиляции ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2012.


    [Meyers1]
    Мейерс, Скотт. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2014.


    [Meyers2]
    Мейерс, Скотт. Эффективный и современный C++: 42 рекомендации по использованию C++11 и C++14.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2016.


    [Sutter]
    Саттер, Герб. Решение сложных задач на C++.: Пер. с англ. — М: ООО «И.Д. Вильямс», 2015.


    [Spolsky]
    Сполски, Джоэл. Джоэл о программировании.: Пер. с англ. — СПб.: Символ-Плюс, 2008.





    Also popular now: