📜 ⬆️ ⬇️

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



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 in the if(...) and similar instructions you can substitute any numeric expression or pointer, and not just expressions of the bool type. The problem is aggravated by the implicit conversion from bool to int in arithmetic expressions and the priority of some operators. This leads, for example, to such errors:


if(a=b) when if(a==b) correct,
if(a<x<b) , if(a<x && x<b) correct if(a<x && x<b) ,
if(a&x==0) when if((a&x)==0) correct,
if(Foo) when correct if(Foo()) ,
if(arr) , if correctly if(arr[0]) ,
if(strcmp(s,r)) when correct 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, if(...) and similar instructions require the bool type, you cannot mix bool and numeric types in arithmetic expressions.


How to fight:




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.


The most annoying implicit conversions are conversions of a numeric type or pointer to bool and from bool to int . It is these transformations (necessary for compatibility with C) that cause the problems described in section 1.1. Also implicit conversions are not always appropriate, potentially causing a loss of precision of numeric data (narrowing conversions), for example, from double to 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 bool forbidden (even explicit), and conversions that potentially cause a loss of precision of numeric data are almost always an error.


A programmer can add other implicit conversions: (1) a single-parameter constructor definition without the explicit keyword; (2) the definition of a type conversion operator. These transformations make additional security holes based on strong typing principles.


In C #, the number of embedded implicit conversions is much smaller; user-defined implicit conversions must be declared using the implicit keyword.


How to fight:




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 ++, the variable , declared in , hides (hide) the variable , declared in The first declaration of x does 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 block


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


 //   {    int x;    // ... //  ,       {    // -         } } 

By mistake, changes are made:


 //   {    //  , :    int x;    // -         // ...    //  :    // -      } 

And now the code “something is done with from ” will have something to do with from ! It is clear that everything does not work the way it used to, and it is often not easy to find what is happening. Not for nothing in C # to hide local variables is prohibited (though 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:




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 come across 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 of functions can reduce the readability of the code, and thus provoke 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:




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 embedded. Details about special member functions can be found in [Meyers2].


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 management of 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 states 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 must apply the construction "=delete" when declaring, 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, the declaration should use the construction "=default" , while the code is better readable and additional features related to access level control appear.


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


How to fight:




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 compiled and executed in C ++, should work in the same way as in C. Trivial types include numeric types, pointers, enumerations, as well as 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 no option of explicit initialization is used. 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 new T() creates an object initialized by the default constructor, while new T for trivial types 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 of std::vector<T> is created or expanded 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 wherever traditional initialization is used, except that 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 arranged as follows: if it is defined in the scope of the namespace (globally), it will have all the bits zero, if it is local, or dynamically created, 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, find uninitialized variables and give an error. Uninitialized code analyzers detect even better.


In the standard C ++ 11 library, there are templates called type properties (header file <type_traits> ). One of them allows you to determine whether a type is trivial. The std::is_trivial<>::value expression is true if T trivial type and false otherwise.


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 reference type objects 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:




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 must be taken into account if, when initializing, 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). .


C# : , , . .


:




3.4.


, , namespace () (), , . , . .


:




3.5.


. , .


:




3.6.


T


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

delete


 delete pt; 


 T* pt = new T[N]; 

delete[]


 delete[] pt; 

, , , : , .. . [Meyers1].


:




3.7.


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:


 class X; //   X* CreateX(); void Foo() {    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


X CreateX() , , CreateX() , new , Foo() , . , , .


, -. , . , , , , . [Meyers2].


:




4. ,



4.1.


++ , . . . , 1.1.


Let's give an example:


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


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


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

, , .


. << ?: std::out void* . ++ , . -, , . ?: . , ( ).


: x&f==0 x&(f==0) , (x&f)==0 , , , . - , , , , .


. / . / , /, . , x/4+1 x>>2+1 , x>>(2+1) , (x>>2)+1 , .


C# , C++, , - .


:




4.2.


++ , . . , , . 4.1. — + += . . , : , (), && , || . , (-), (short-circuit evaluation semantics), , . & ( ). & , .. .


, - (-) , . .


- , , . . [Dewhurst].


C# , , , .


:




4.3.


++ , . ( : , (), && , || , ?: .) , , , . :


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

y .


, . .

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

Foo() :


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

: X , Y , std::shared_ptr<X> , std::shared_ptr<Y> . Y , X .


:


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

std::make_shared<Y> ( , ):


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

. [Meyers2].


:




5.



5.1.


++98 , ( ), , ( , ). virtual , , . ( ), , , . , , . , ++11 override , , , . .


:




5.2.


. , , . . . [Dewhurst].


:




5.3.


, , . , , post_construct pre_destroy. , — . . , : ( ) . (, , .) , ( ), ( ). . [Dewhurst]. , , .


— - .


, C# , , , . C# : , , . , ( , ).


:




5.4.


, , delete . , - .


:




6.


— C/C++, . . . « ».


C# unsafe mode, .



6.1.


/++ , : strcpy() , strcat() , sprinf() , etc. ( std::vector<> , etc.) , . (, , , . . Checked Iterators MSDN.) , : , , ; , .


C#, unsafe mode, .


:




6.2. Z-terminated


, . , :


 strncpy(dst,src,n); 

strlen(src)>=n , dst (, ). , , . . — . if(*str) , if(strlen(str)>0) , . [Spolsky].


C# string .


:




6.3.


... . printf - , C. , , , , . , .


C# printf , .


:




7.



7.1.


++ , , , . Here is an example:


 const int 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 

:


  1. int ;
  2. int ;
  3. N int ;
  4. N int ;
  5. , char int ;
  6. , char int ;
  7. , char int ;
  8. N , char int ;
  9. N int ;
  10. M N int ;
  11. , char , long int .

, . ( .)


* & . ( .)


typedef ( using -). , :


 typedef int(*P)(long); PH(char); 

, .


C# , .


:




7.2.


.


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


 X x(5); 

x X , 5.


 X x(); 

x , X , x X , . X , , :


 X x; X x = X(); X x{};    //   C++11 

, , , . [Sutter].


, , C++ ( ). . ( C++ .)


, , , , .


C# , , .


:




8.



8.1. inline ODR


, inline — . , . inline (One Defenition Rule, ODR). . , . , ODR. static : , , . static inline . , , ODR, . , . - , -. .


:




8.2.


. . , , , , .


:




8.3. switch


— break case . ( .) C# .


:




8.4.


++ , — , — . ( class struct ) , . ( , # Java.) — , .


  1. , . ( std::string , std::vector , etc.), , .
  2. , , .
  3. , (slicing), , .

, , , . . , , . , . . — ( =delete ), — explicit .


C# , .


:




8.5.


++ . , . - ( ), ++11 , , , .


C++ .


C# , . , . (using-) Basic Dispose.


:




8.6.


«» . , , C++ , STL- - .


. . , . . «», . COM- . (, .) , C++ . — . . . , («» ) , . .


# , . — .


:




8.7.


C++ , : , , . ( !) . , . , . , , . (, .)


C ( ), C++ C ( extern "C" ). C/C++ .


-. #pragma - , , .


, , , .


, , COM. COM-, , ( , ). COM , , .


C# . , — , C#, C# C/C++.


:




8.8. Macros


, . , . C++ . Instead


 #define XXL 32 


 const int XXL=32; 

. inline .


# ( ).


:




9.


  1. . . . , .
  2. .
  3. . ++ — ++11/14/17.
  4. - , - .
  5. .



List

[Dewhurst]
, . C++. .: . from English — .: , 2012.


[Meyers1]
, . C++. 55 .: . from English — .: , 2014.


[Meyers2]
, . C++: 42 C++11 C++14.: . from English — .: «.. », 2016.


[Sutter]
, . C++.: . from English — : «.. », 2015.


[Spolsky]
, . .: . from English — .: -, 2008.





')

Source: https://habr.com/ru/post/428898/


All Articles