12. Special Member Functions
12.1 Conversions
12.1.1 Do not declare implicit user defined conversions
A user-defined conversion can occur through the use of a conversion operator or a conversion constructor (a constructor that accepts a single argument). A compiler can invoke a single user defined conversion in a standard conversion sequence, but only if the operator or constructor is declared without the explicit keyword. It is better to declare all conversion constructors and operators explicit.
#include <cstdint> class C { public: C (const C&); // @@+ Compliant: copy constructor +@@ C (); // @@+ Compliant: default constructor +@@ C (int32_t, int32_t); // @@+ Compliant: more than one non-default argument +@@ explicit C (int32_t); // @@+ Compliant +@@ C (double); // @@- Non-Compliant -@@ C (float f, int32_t i = 0); // @@- Non-Compliant -@@ C (int32_t i = 0, float f = 0.0); // @@- Non-Compliant: default constructor, -@@ // @@- but also a conversion constructor -@@ operator int32_t () const; // @@- Non-Compliant -@@ explicit operator double () const; // @@+ Compliant +@@ };
References
- HIC++ v3.3 – 3.2.3
- HIC++ v3.3 – 3.1.10
- HIC++ v3.3 – 3.1.11
12.2 Destructors
12.2.1 Declare virtual, private or protected the destructor of a type used as a base class
If an object will ever be destroyed through a pointer to its base class, then the destructor in the base class should be virtual. If the base class destructor is not virtual, then the destructors for derived classes will not be invoked. Where an object will not be deleted via a pointer to its base, then the destructor should be declared with protected or private access. This will result in a compile error should an attempt be made to delete the object incorrectly.
#include <cstdint> class A { public: ~A (); // @@- Non-Compliant -@@ }; class B : public A { }; class C { public: virtual ~C (); // @@+ Compliant +@@ }; class D : public C { }; class E { protected: ~E (); // @@+ Compliant +@@ }; class F : public E { };
References
- HIC++ v3.3 – 3.3.2
12.3 Free Store
12.3.1 Correctly declare overloads for operator new and delete
Operator new and operator delete should work together. Overloading operator new means that a custom memory management scheme is in operation for a particular class or program. If a corresponding operator delete (plain or array) is not provided the memory management scheme is incomplete. Additionally, if initialization of the allocated object fails with an exception, the C++ runtime will try to call an operator delete with identical parameters as the called operator new, except for the first parameter. If no such operator delete can be found, the memory will not be freed. If this operator delete does not actually need to perform any bookkeeping, one with an empty body should be defined to document this in the code. When declared in a class, operator new and operator delete are implicitly static members; explicitly including the static specifier in their declarations helps to document this.
#include <cstddef> class C { public: static void* operator new (std::size_t size); static void operator delete (void* ptr); // @@+ Compliant +@@ void* operator new [] (std::size_t size); // @@- Non-Compliant -@@ };
References
- HIC++ v3.3 – 12.6
- HIC++ v3.3 – 12.7
12.4 Initializing Bases and Members
12.4.1 Do not use the dynamic type of an object unless the object is fully constructed
Expressions involving:
- A call to a virtual member function,
- Use of typeid, or
- A cast to a derived type using dynamic_cast
These are said to use the dynamic type of the object. Special semantics apply when using the dynamic type of an object while it is being constructed or destructed. Moreover, it is undefined behavior if the static type of the operand is not (or is not a pointer to) the constructor’s or destructor’s class or one of its base classes. In order to avoid misconceptions and potential undefined behavior, such expressions should not be used while the object is being constructed or destructed.
#include <typeinfo> class A { public: virtual void foo (); virtual void bar (); }; class B : public A { public: B(); ~B(); void foo () override; }; class C : public B { public: void foo () override; void bar () override; }; B::B() { foo (); // @@- Non-Compliant: B::foo () is called and never C::foo () -@@ B::foo (); // @@+ Compliant: not a virtual call +@@ typeid (*this); // @@- Non-Compliant -@@ } B::~B() { bar (); // @@- Non-Compliant: A::bar () is called and never C::bar () -@@ A::bar (); // @@+ Compliant: not a virtual call +@@ dynamic_cast <A*> (this); // @@+ Compliant: dynamic type is not needed for a downcast +@@ dynamic_cast <C*> (this); // @@- Non-Compliant -@@ } void foo () { C c; }
References
- HIC++ v3.3 – 3.3.13
- JSF AV C++ Rev C – 71
- MISRA C++:2008 – 12-1-1
12.4.2 Ensure that a constructor initializes explicitly all base classes and non-static data members
A constructor should completely initialize its object. Explicit initialization reduces the risk of an invalid state after successful construction. All virtual base classes and direct non-virtual base classes should be included in the initialization list for the constructor. A copy or move constructor should initialize each non-static data member in the initialization list, or if this is not possible then in constructor body. For other constructors, each non-static data member should be initialized in the following way, in order of preference:
- Non-static data member initializer (NSDMI).
- In initialization list.
- In constructor body.
For many constructors, this means that the body becomes an empty block.
class C { public: C () : m_j (0), m_a () {} // @@+ Compliant +@@ // @@- Non-Compliant: m_a not initialized -@@ C (C const & other) : m_i (other.m_i), m_j (other.m_j) {} explicit C (int32_t j) : m_j (j) {} // @@- Non-Compliant: m_a not initialized -@@ private: int32_t m_i = 0; int32_t m_j; int32_t m_a [10]; };
References
- HIC++ v3.3 – 3.2.1
12.4.3 Do not specify both an NSDMI and a member initializer in a constructor for the same non static member
NSDMI stands for ‘non static data member initializer’. This syntax, introduced in the 2011 C++ Language Standard, allows for the initializer of a member to be specified along with the declaration of the member in the class body. To avoid confusion as to the value of the initializer actually used, if a member has an NSDMI then it should not subsequently be initialized in the member initialization list of a constructor.
#include <cstdint> class A { public: A() : m_i1(1) // @@+ Compliant +@@ , m_i2(1) // @@- Non-Compliant -@@ { } private: int m_i1; int m_i2 = 0; // @@- Non-Compliant -@@ int m_i3 = 0; // @@+ Compliant +@@ };
Exception
The move/copy constructors are exempt from this rule, as in this context the rule conflicts with Rule 12.4.2: ”Ensure that a constructor initializes explicitly all base classes and non-static data members”, which takes precedence.
For Example:
# include <cstdint> class A { public : A(A const & rhs) : m_i1 (rhs. m_i1 ) // Compliant , m_i2 (rhs . m_i2 ) // Compliant { } private : int32_t m_i1 ; int32_t m_i2 = 0; };
12.4.4 Write members in an initialization list in the order in which they are declared
Regardless of the order of member initializers in a initialization list, the order of initialization is always:
- Virtual base classes in depth and left to right order of the inheritance graph.
- Direct non-virtual base classes in left to right order of inheritance list.
- Non-static member data in order of declaration in the class definition.
To avoid confusion and possible use of uninitialized data members, it is recommended that the initialization list matches the actual initialization order.
#include <cstdint> class B {}; class VB : public virtual B {}; class C {}; class DC : public VB, public C { public: DC() : B(), VB(), C(), i (1), c() // @@+ Compliant +@@ {} private: int32_t i; C c; };
References
- HIC++ v3.3 – 3.2.2
12.4.5 Use delegating constructors to reduce code duplication
Delegating constructors can help reduce code duplication by performing initialization in a single constructor. Using delegating constructors also removes a potential performance penalty with using an ‘init’ method, where initialization for some members occurs twice.
#include <cstdint> // @@- Non-Compliant -@@ class A1 { public: A1() { init(10, 20); } A1(int i) { init(i, 20); } private: void init(int32_t i, int32_t j); private: int32_t m_i; int32_t m_j; }; // @@+ Compliant +@@ class A2 { public: A2() : A2(10, 20) { } A2(int32_t i) : A2(i, 20) { } private: A2(int32_t i, int32_t j) : m_i(i) , m_j(j) { } private: int32_t m_i; int32_t m_j; };
12.5 Copying and Moving Class Objects
12.5.1 Define explicitly =default or =delete implicit special member functions of concrete classes
A compiler may provide some or all of the following special member functions:
- Destructor
- Copy constructor
- Copy assignment operator
- Move constructor
- Move assignment operator
The set of functions implicitly provided depends on the special member functions that have been declared by the user and also the special members of base classes and member objects. The compiler generated versions of these functions perform a bitwise or shallow copy, which may not be the correct copy semantics for the class. It is also not clear to clients of the class if these functions can be used or not. To resolve this, the functions should be defined with =delete or =default thereby fully documenting the class interface.
#include <cstdint> class A1 // @@- Non-Compliant: Includes implicitly declared special members -@@ { public: A1(); ~A1(); private: int32_t * m_i; }; class A2 // @@+ Compliant: No implicitly declared special members +@@ { public: A2(); ~A2(); A2(A2 const &) = default; A2 & operator=(A2 const &) & = delete; private: int32_t * m_i; };
Note: As this rule is limited to concrete classes, it is the responsibility of the most derived class to ensure that the object has correct copy semantics for itself and for its sub-objects.
References
- HIC++ v3.3 – 3.1.3
- HIC++ v3.3 – 3.1.13
12.5.2 Define special members =default if the behavior is equivalent
The implicitly defined copy constructor for a class X with two bases and two members will be defined as:
Copy Constructor:
X::X(X const & rhs) : base1 (rhs) , base2 (rhs ) , mbr1 (rhs . mbr1 ) , mbr2 (rhs . mbr2 ) { }
The implicitly defined move constructor for the same class X will be defined as:
Move Constructor:
X::X(X && rhs) : base1 (std :: move (rhs )) , base2 (std :: move (rhs )) , mbr1 (std :: move (rhs . mbr1 )) , mbr2 (std :: move (rhs . mbr2 )) { }
Finally, the implicitly defined destructor will be defined as:
Destructor:
X::~X() { }
If a class contains a user defined version of a member with the same definition as would be provided by the compiler, then it will be less error prone and more maintainable to replace the definition with =default.
#include <cstdint> class A { public: ~A() // @@- Non-Compliant -@@ { } A(A const & rhs) // @@- Non-Compliant -@@ : mbr(rhs.mbr) { } A(A &&) = default; // @@+ Compliant +@@ private: int32_t mbr; };
References
- JSF AV C++ Rev C – 80
12.5.3 Ensure that a user defined move/copy constructor only moves/copies base and member objects
The human clients of a class will expect that the copy constructor can be used to correctly copy an object of class type. Similarly, they will expect that the move constructor correctly moves an object of class type. Similarly, a compiler has explicit permission in the C++ Standard to remove unnecessary copies or moves, on the basis that these functions have no other side-effects other than to copy or move all bases and members.
#include <cstdint> #include <utility> class Base { public: Base () : m_j (-1) { } Base (Base const & rhs) : m_j (rhs.m_j) { } Base (Base && rhs) noexcept : m_j (std::move (rhs.m_j)) { } private: int32_t m_j; }; void foo (); class Derived1 : public Base { public: Derived1 (Derived1 const & rhs) : Base (rhs) , m_i (rhs.m_i) { foo (); // @@- Non-Compliant: unrelated side effect -@@ } Derived1 (Derived1 && rhs) noexcept : Base (std::move (rhs)) , m_i (std::move (rhs.m_i)) { foo (); // @@- Non-Compliant: unrelated side effect -@@ } private: int32_t m_i; }; class Derived2 : public Base { public: Derived2 (Derived2 const & rhs) // @@+ Compliant +@@ : Base (rhs) , m_i (rhs.m_i) { } Derived2 (Derived2 && rhs) noexcept // @@+ Compliant +@@ : Base (std::move (rhs)) , m_i (std::move (rhs.m_i)) { } private: int32_t m_i; };
References
- MISRA C++:2008 – 12-8-1
12.5.4 Declare noexcept the move constructor and move assignment operator
A class provides the Strong Exception Guarantee if after an exception occurs, the objects maintain their original values. The move members of a class explicitly change the state of their argument. Should an exception be thrown after some members have been moved, then the Strong Exception Guarantee may no longer hold as the from object has been modified. It is especially important to use noexcept for types that are intended to be used with the standard library containers. If the move constructor for an element type in a container is not noexcept then the container will use the copy constructor rather than the move constructor.
#include <utility> #include <cstdint> class B { public: B (B && rhs) noexcept // @@+ Compliant +@@ : m_p (std::move (rhs.m_p)) { rhs.m_p = 0; } private: int32_t * m_p; }; class A { public: A (A && rhs) // @@- Non-Compliant -@@ : m_a (std::move (rhs.m_a)) , m_b ((rhs.m_b) ? std::move (rhs.m_b) : throw 0) { } A& operator = (A&&) noexcept; // @@+ Compliant +@@ private: B m_a; int32_t m_b; };
12.5.5 Correctly reset moved-from handles to resources in the move constructor
The move constructor moves the ownership of data from one object to another. Once a resource has been moved to a new object, it is important that the moved-from object has its handles set to a default value. This will ensure that the moved-from object will not attempt to destroy resources that it no longer manages on its destruction. The most common example of this is to assign nullptr to pointer members.
#include <utility> #include <cstdint> class A1 { public: A1(A1 && rhs) noexcept // @@- Non-Compliant -@@ : m_p(std::move(rhs.m_p)) { } ~A1() { delete m_p; // Moved-from object will attempt to delete // resource owned by other object } private: int32_t * m_p; }; class A2 { public: A2(A2 && rhs) noexcept // @@+ Compliant +@@ : m_p(std::move(rhs.m_p)) { rhs.m_p = nullptr; } ~A2() { delete m_p; // Moved-from object will not delete // resource owned by other object } private: int32_t * m_p; };
12.5.6 Use an atomic, non-throwing swap operation to implement the copy and move assignment operators
Implementing the copy assignment operator using a non throwing swap provides the Strong Exception Guarantee for the operations. In addition, the implementation of each assignment operator is simplified without requiring a check for assignment to self.
#include <utility> #include <cstdint> class A1 { public: A1(A1 const & rhs) : m_p1(new int32_t (*rhs.m_p1)) , m_p2(new int32_t (*rhs.m_p2)) { } A1(A1 && rhs) noexcept : m_p1(std::move (rhs.m_p1)) , m_p2(std::move (rhs.m_p2)) { rhs.m_p1 = nullptr; rhs.m_p2 = nullptr; } ~A1() { delete m_p1; delete m_p2; } A1 & operator=(A1 const & rhs) & // @@- Non-Compliant -@@ { if (this != &rhs) { m_p1 = new int32_t (*rhs.m_p1); // An exception here would result in a memory leak for m_p1 m_p2 = new int32_t (*rhs.m_p2); } return *this; } A1 & operator=(A1 && rhs) & noexcept // @@- Non-Compliant -@@ { if (this != &rhs) { m_p1 = std::move (rhs.m_p1); m_p2 = std::move (rhs.m_p2); rhs.m_p1 = nullptr; rhs.m_p2 = nullptr; } return *this; } private: int32_t * m_p1; int32_t * m_p2; }; class A2 { public: A2(A2 const & rhs) : m_p1(new int32_t (*rhs.m_p1)) , m_p2(new int32_t (*rhs.m_p2)) { } A2(A2 && rhs) noexcept : m_p1(std::move (rhs.m_p1)) , m_p2(std::move (rhs.m_p2)) { rhs.m_p1 = nullptr; rhs.m_p2 = nullptr; } ~A2() { delete m_p1; delete m_p2; } A2 & operator=(A2 rhs) & // @@+ Compliant: Note: 'rhs' is by value +@@ { swap (*this, rhs); return *this; } A2 & operator=(A2 && rhs) & noexcept // @@+ Compliant +@@ { A2 tmp (std::move (rhs)); swap (*this, tmp); return *this; } void swap(A2 & lhs, A2 & rhs) noexcept { std::swap (lhs.m_p1, rhs.m_p1); std::swap (lhs.m_p2, rhs.m_p2); } private: int32_t * m_p1; int32_t * m_p2; };
References
- HIC++ v3.3 – 3.1.4
12.5.7 Declare assignment operators with the ref-qualifier &
In the 2003 C++ language standard, user declared types differed from built-in types in that it was possible to have a ‘modifiable rvalue’.
#include <cstdint> class A { public: A(); A & operator*=(int32_t); // @@- Non-Compliant -@@ }; A f1(); int32_t f2(); int main () { f1() *= 10; // Temporary result of 'f()' multiplied by '10' f2() *= 10; // Compile error }
The 2011 C++ language standard allows for a function to be declared with a reference qualifier. Adding & to the function declaration ensures that the call can only be made on lvalue objects, as is the case for the built-in operators.
#include <cstdint> class A { public: A(); A & operator*=(int32_t) &; // @@+ Compliant +@@ }; A f1(); int32_t f2(); int main () { f1() *= 10; // Compile error f2() *= 10; // Compile error }
References
- HIC++ v3.3 – 3.1.5
12.5.8 Make the copy assignment operator of an abstract class protected or define it =delete
An instance of an abstract class can only exist as a subobject for a derived type. A public copy assignment operator would allow for incorrect partial assignments to occur. The copy assignment operator should be protected, or alternatively defined =delete if copying is to be prohibited in this class hierarchy.
#include <cstdint> class A { public: virtual ~A () = 0; A& operator = (A const &) &; // @@- Non-Compliant -@@ }; class AA : public A { }; void foo() { AA obj1; AA obj2; A* ptr1 = &obj1; A* ptr2 = &obj2; *ptr1 = *ptr2; // partial assignment } class B { public: virtual ~B () = 0; protected: B& operator = (B const &) &; // @@+ Compliant +@@ }; class C { public: virtual ~C () = 0; protected: C& operator = (C const &) & = default; // @@+ Compliant +@@ }; class D { public: virtual ~D () = 0; D& operator = (D const &) & = delete; // @@+ Compliant +@@ };
References
- HIC++ v3.3 – 3.3.14