...
Part 4. Classes
...
This article is a translation of a part of Google's C ++ style guide into Russian.
Original article (fork on github), updated translation .
Classes
Classes are the main building block in C ++. And, of course, they are often used. This section describes the basic rules and prohibitions to follow when using classes.
Code in the constructor
Don't call virtual methods in the constructor. Avoid initialization that can fail (and there is no way to signal an error. Note: Note that Google doesn't like exceptions).
Definition
In general, any initialization can be performed in a constructor (i.e. all initialization can be done in a constructor).
Per
- No need to worry about uninitialized class.
- Objects that are fully initialized in the constructor can be const and are also easier to use in standard containers and algorithms.
Vs
- If virtual functions are called in the constructor, then implementations from the derived class are not called. Even if the class now has no descendants, this may become a problem in the future.
- ( ) ( ).
- , ( — ) . : bool IsValid(). .
- . , , .
Verdict
Constructors should not call virtual functions. In some cases (if allowed), design errors can be handled through program termination. Otherwise, consider the Factory Method pattern or use Init () (more details here: TotW # 42 ). Use Init () only if the object has state flags that allow calling certain public functions (since it is difficult to fully work with a partially constructed object).
Implicit conversions
Don't declare implicit conversions. Use the explicit keyword for type conversion operators and single-argument constructors.
Definition
Implicit conversions allow an object of one source type to be used where another type (destinationtype) is expected, such as passing an argument of type int to a function expecting a double .
In addition to the implicit conversions specified by the programming language, you can also define your own custom ones by adding the appropriate members to the class declaration (both source and destination). Source-side implicit conversion is declared as operator + receiver type (for example, operator bool () ). The implicit conversion on the receiver side is implemented by a constructor that takes the source type as the only argument (in addition to the default arguments).
The explicit keyword can be applied to a constructor or a conversion operator to explicitly indicate that a function can only be used when there is an explicit type match (for example, after a cast operation). This applies not only for implicit conversions, but also for initialization lists in C ++ 11:
class Foo {
explicit Foo(int x, double y);
...
};
void Func(Foo f);
Func({42, 3.14}); //
This code example is technically not an implicit conversion, but the language treats it as if it were meant to be explicit .
Per
- , .
- , string_view std::string const char*.
- .
- , ( ).
- , : , .
- , .
- explicit : , .
- , . — , .
- , , , .
Verdict
Conversion operators and single-argument constructors must be declared with the explicit keyword . There is also an exception: copy and move constructors can be declared without explicit , since they do not perform type conversion. Also, implicit conversions may be necessary in the case of wrapper classes for other types (in this case, be sure to ask your upstream management permission to ignore this important rule).
Constructors that cannot be called with a single argument can be declared without explicit . Constructors accepting a single std :: initializer_listmust also be declared without explicit to support copy-initialization (for example, MyType m = {1, 2}; ).
Copyable and Relocatable Types
The public interface of the class must explicitly indicate the ability to copy and / or move, or vice versa, prohibit everything. Support copying and / or moving only if these operations make sense for your type.
Definition A
relocatable type is one that can be initialized or assigned from temporary values.
Copyable type - can be initialized or assigned from another object of the same type (i.e., the same as the relocatable one), provided that the original object remains unchanged. For example , std :: unique_ptr <int> is relocatable, but not the type to be copied (because the value of the original std :: unique_ptr <int> object must change when assigned to the target). intand std :: string are examples of relocatable types that can also be copied: for int the move and copy operations are the same, for std :: string the move operation requires less resources than copy.
For user-defined types, copying is specified by the copy constructor and the copy operator. The movement is specified either by the move constructor with the move operator, or (if not present) by the corresponding copy functions.
Copy and move constructors can be implicitly called by the compiler, for example, when passing objects by value.
Per
Objects of copyable and relocatable types can be passed and received by value, which makes the API simpler, safer, more versatile. In this case, there are no problems with the ownership of the object, its life cycle, value change, etc., and it is also not required to specify them in the "contract" (all this is unlike passing objects by pointer or reference). Lazy communication between the client and the implementation is also prevented, making the code much easier to understand, maintain, and optimize by the compiler. Such objects can be used as arguments to other classes that require passing by value (for example, most containers), and in general they are more flexible (for example, when used in design patterns).
Copy / move constructors and associated assignment operators are usually easier to define than alternatives like Clone () , CopyFrom (), or Swap () because the compiler can generate the required functions (implicitly or with = default ). They (functions) are easy to declare and you can be sure that all class members will be copied. Constructors (copy and move) are generally more efficient because do not require memory allocation, separate initialization, additional assignments, are well optimized (see copy elision ).
Move operators allow you to efficiently (and implicitly) manipulate rvalue resources of objects. This sometimes makes coding easier.
Against
Some types are not required to be copyable, and support for copying operations may be counter-intuitive or cause incorrect operation. Types for singletones ( Registerer ), objects for cleaning (for example, when going out of scope) ( Cleanup ) or containing unique data ( Mutex ) are, in their meaning, non-copyable. Also, copying operations for base classes that have descendants can lead to object slicing... The default (or poorly written) copy operations can lead to errors that are difficult to detect.
Copy constructors are called implicitly and this is easy to overlook (especially for programmers who previously wrote in languages where objects are passed by reference). You can also reduce performance by making unnecessary copies.
Verdict The
public interface of each class must explicitly indicate which copy and / or move operations it supports. This is usually done in the public section in the form of explicit declarations of the required functions or by declaring them as delete.
In particular, the copied class must explicitly declare copy operations; only a relocatable class must explicitly declare move operations; an uncopyable / unmovable class must explicitly deny ( = delete ) copy operations. Explicitly declaring or deleting all four copy and move functions is also allowed, although not required. If you implement the copy and / or move operator, then you must also make the corresponding constructor.
class Copyable {
public:
Copyable(const Copyable& other) = default;
Copyable& operator=(const Copyable& other) = default;
// (.. )
};
class MoveOnly {
public:
MoveOnly(MoveOnly&& other);
MoveOnly& operator=(MoveOnly&& other);
// . ( ) :
MoveOnly(const MoveOnly&) = delete;
MoveOnly& operator=(const MoveOnly&) = delete;
};
class NotCopyableOrMovable {
public:
//
NotCopyableOrMovable(const NotCopyableOrMovable&) = delete;
NotCopyableOrMovable& operator=(const NotCopyableOrMovable&)
= delete;
// (), :
NotCopyableOrMovable(NotCopyableOrMovable&&) = delete;
NotCopyableOrMovable& operator=(NotCopyableOrMovable&&)
= delete;
};
Described function declarations or deletions can be omitted in obvious cases:
- If a class does not contain a private section (for example, a struct or an interface class), then copyability and relocation can be declared through a similar property of any public member.
- , . , , .
- , () /, / (.. ). / . .
A type should not be declared copyable / relocatable unless the ordinary programmer understands the need for these operations, or if the operations are very resource and performance intensive. Move operations for copied types are always a performance optimization, but on the other hand they are a potential source of bugs and complications. Therefore, do not declare move operations unless they provide significant performance gains over copying. In general, it is desirable (if copy operations are declared for a class) to design everything so that the default copy functions are used. And be sure to check the correctness of any operations by default.
Because of the risk of "slicing", it is preferable to avoid public copy and move statements for classes that you plan to use as base classes (and preferably not inherit from a class with such functions). If you need to make the base class copyable, then make a public virtual function Clone () and a protected copy constructor so that the derived class can use them to implement copy operations.
Structures vs Classes
Use structures ( struct ) only for passive objects that store data. In other cases, use the classes ( class ).
The struct and class keywords are almost identical in C ++. However, we have our own understanding for each keyword, so use the one that suits its purpose and meaning.
Structures should be used for passive objects, only for data transfer. They can have constants of their own, but there shouldn't be any functionality (with the possible exception of get / set functions). All fields must be public, available for direct access, and this is preferable to using get / set functions. Structures should not contain invariants (for example, computed values), which are based on dependencies between different fields of the structure: the ability to directly modify the fields can invalidate the invariant. Methods should not restrict the use of the structure, but can assign values to fields: i.e. as a constructor, destructor or functions Initialize () , Reset () .
If additional functionality is required in data processing or invariants, it is preferable to use the classes ( class ). Also, if in doubt which to choose - use classes.
In some cases ( template meta-functions , traits, some functors) for consistency with the STL it is allowed to use structures instead of classes.
Remember that variables in structures and classes are named in different styles.
Structures vs pairs and tuples
If individual elements in a data block can be meaningfully named, then it is desirable to use structures instead of pairs or tuples.
While using pairs and tuples avoids reinventing the wheel with your own type and will save you a lot of time writing code, fields with meaningful names (instead of .first , .second, or std :: get <X> ) will be easier to read when reading the code. And although C ++ 14 adds type access ( std :: get <Type> , and the type must be unique) for tuples in addition to index access , the field name is much more informative than the type.
Pairs and tuples are fine in code where there is no special distinction between the elements of a pair or tuple. They are also required to work with existing code or APIs.
Inheritance
Class composition is often more appropriate than inheritance. When using inheritance, make it public .
Definition
When a child class inherits from a base class, it includes the definitions of all data and operations from the base. "Interface inheritance" is inheritance from a pure abstract base class (no state or methods are defined in it). Everything else is "implementation inheritance".
Per
Implementation inheritance reduces code size by reusing portions of the base class (which becomes part of the new class). Because inheritance is a compile-time declaration, it allows the compiler to understand the structure and find errors. Interface inheritance can be used to make the class support the required API. And also, the compiler can find errors if the class does not define the required method of the inherited API.
Cons
In the case of implementation inheritance, the code begins to blur between the base and child classes and this can complicate the understanding of the code. Also, the child class cannot override the code of non-virtual functions (cannot change their implementation).
Multiple inheritance is even more problematic and sometimes leads to performance degradation. Often, the performance penalty when moving from single inheritance to multiple inheritance can be greater than the transition from regular functions to virtual ones. Also, there is one step from multiple inheritance to rhombic inheritance, and this already leads to ambiguity, confusion and, of course, bugs.
Verdict
Any inheritance must be public . If you want to make it private , it is better to add a new member with an instance of the base class.
Don't overuse implementation inheritance. Class composition is often preferred. Try to limit the use of inheritance semantics "is»: Bar , you can inherit from the Foo , if I may say that the Bar "is» the Foo (ie, where used the Foo , you can also use the Bar ).
Protected ( protected, ) do only those functions that should be available for the child classes. Note that the data must be private.
Explicitly declare virtual function / destructor overrides using specifiers: either override or (if required) final . Do not use the virtual specifier when overriding functions. Explanation: A function or destructor that is marked override or final but is not virtual will simply not compile (which helps catch common errors). Also specifiers work like documentation; and if there are no specifiers, the programmer will be forced to check the entire hierarchy to clarify the virtuality of the function.
Multiple inheritance is allowed, however multiple inheritance implementation is not recommended from the word at all.
Operator overloading
Overload operators as reasonably as possible. Don't use custom literals.
Determination of
C ++ code allows the user to override the built-in operators using the keyword operator and user type as one of the parameters; also operator allows you to define new literals using operator "" ; you can also create casting functions like operator bool () .
Per
Using operator overloading for user-defined types (similar to built-in types) can make your code more concise and intuitive. Overloaded operators correspond to certain operations (for example, == , < , = and << ) and if the code follows the logic of applying these operations, then user-defined types can be made clearer and used when working with external libraries that rely on these operations.
Custom literals are a very efficient way to create custom objects.
Vs
- (, ) — , , .
- , .
- , , .
- , , .
- , .
- / ( ), «» . , foo < bar &foo < &bar; .
- . & , . &&, || , () ( ) .
- , . , .
- (UDL) , C++ . : «Hello World»sv std::string_view(«Hello World»). , .
- Because no namespace is specified for the UDL, you will either need to use a using directive (which is prohibited ) or a using declaration (which is also prohibited (in header files) , unless the imported names are part of the interface shown in the header file). For such header files, it is best to avoid UDL suffixes, and it is desirable to avoid dependencies between literals that are different in the header and source file.
Verdict
Define overloaded operators only if their meaning is clear, understandable, and consistent with the general logic. For example, use | in the sense of the OR operation; implementing pipe logic instead is not a good idea.
Define operators only for your own types, do so in the same header and source file, and in the same namespace. As a result, operators will be available in the same place as the types themselves, and the risk of multiple definitions is minimal. Whenever possible, avoid defining operators as templates. you have to match any set of template arguments. If you define an operator, also define "siblings" to it. And take care of the consistency of the results they return. For example, if you define the < operator , then define all comparison operators and make sure that the < and > operators never return true for the same arguments.
It is desirable to define immutable binary operators as external functions (non-members). If the binary operator is declared a member of the class, the implicit conversion can be applied to the right argument, but not to the left one. This can be a little frustrating for programmers if (for example) the code a <b will compile, but b <a will not.
No need to try to bypass operator overrides. If comparison (or assignment and output function) is required, then it is better to define == (or = and << ) instead of your Equals () , CopyFrom () and PrintTo () . Conversely, you don't need to redefine an operator just because external libraries expect it. For example, if the data type cannot be ordered and you want to store it in std :: set , then it is better to make a custom comparison function and do not use the < operator .
Do not override && , || , , (Comma) or unary & . Don't override operator "" , i.e. don't introduce your own literals. Do not use previously defined literals (including the standard library and beyond).
Additional information:
Type conversion is described in the section on implicit conversions . The = operator is written in the copy constructor . The topic of overloading << for working with streams is covered in streams . You can also familiarize yourself with the rules from the section on function overloading , which are also suitable for operators.
Accessing class members
Always make class data private , except for constants . This simplifies the use of invariants by adding simplest (often constant) accessor functions.
It is permissible to declare class data as protected for use in test classes (for example, when using Google Test ) or other similar cases.
Announcement procedure
Place similar ads in one place, bring common parts up.
The class definition usually starts with a section of the public: , goes further protected: and then the private: . Do not specify empty sections.
Within each section, group similar declarations together. The preferred order is types (including typedef , using , nested classes and structures), constants, factory methods, constructors, assignment operators, destructors, other methods, data members.
Do not place bulky method definitions in the class definition. Usually only trivial, very short, or performance-critical methods are "inlined" into the class definition. See also Inline Functions .