parts/6.06.ConversionErrors-FLC.md

6.6 Conversion Errors [FLC]

6.6.1 Applicability to language

The vulnerability as documented in ISO IEC 24772-1 clause 6.6 applies to C++. C++ includes some of the conversion mechanisms of C documented in TR 24772-3 clause 6.6.1, however C++ type conversion mechanisms differ from the mechanisms of C, as documented in ISO IEC 14882 Annex C. This subclause highlights differences where C++ provides mitigations of potential vulnerabilities found in C.

In C++, some conversions are explicit while others are implicit. Conversions can change the size of a type, whether or not the type is signed, and possibly other properties of the type. A narrowing conversion is when the target type cannot represent all the values of the original type. Many errors are associated with implicit conversions. For a comprehensive overview see clause 7.3 [conv] of [C++20]

Explicit conversions use one of the mechanisms provided by C++ through a

In C++, a C-style cast is defined in terms of the C++ cast operators const_cast, static_cast, and reinterpret_cast. In some cases, it is unspecified which cast is used, for example when a cast operation involves an incomplete type, a reinterpret_cast may be used for the conversion which can produce an incorrect result.

Unlike C++'s other cast notations, dynamic_cast relies on run-time type information generated by the compiler to ensure the requested conversion is valid. If it is not valid, then nullptr is returned for pointer types, otherwise an exception is thrown. [C++17, Clause 8.2.7 [expr.dynamic.cast]] Thus, dynamic_cast is safer to use when converting down a hierarchy where the base class has virtual member functions. (see Pointer Type Conversions [HFC] and Polymorphic Variables [BKK])

An implicit conversion to a class type can occur for a class with constructors that can be invoked with a single argument, as in the following example:

class C
      {public:
        C(int x=10, float y=0){...}
      };

void foo(C param){...}
void bar( bool b){ foo(b);} 

In the example above, it can be surprising that foo() is called with a boolean.

Note that this implicit conversion to a class object is the default behaviour of constructors that can be called with a single parameter. The explicit keyword can be used before the constructor to prevent this happening, as in:

explicit C(int x=10, float y=0){...}

The call foo(b) would now not be legal.

Implications of casting away const using const_cast are described in section Modifying Constants [UJO].

Other implicit conversions can sometimes result in data loss or erroneous values. This is an issue with implicit conversions since they are automatic: the programmer does not explicitly write code to do the conversion. For example, a common problem is mixing signed and unsigned integral types in arithmetic expressions. This can become a problem since the ranges of signed and unsigned integer types differ and the behaviour of signed integer arithmetic on overflow is undefined whereas unsigned integer arithmetic wraps on overflow. See subclause 4.2 for a discussion of integral promotions in C++.

The issue is not restricted to narrowing conversions, as shown below:

  long l_64 = i_32 + i_32; // '+' operation preformed in 32 bits
                         // widened after the operation completes (and potentially overflows). 

This can be avoided by converting at least one operand to the wider type as part of the operation. Note that auto directs the compiler to use the appropriate type based on the initializer expression. Subsequent use of the auto object (such as in standard mathematical operations) can lead to implicit conversions that are not obvious in the context local to the expression. Additional problems arise as a result of implicit conversions between bool and other types, thus hiding the fact when a wrong operator is used accidentally:

auto f(unsigned i, unsigned j)
{
  return (i > 1) & (j = 1); // (>>, &&, ==) ?
}

In the example above, all combinations of the corresponding operators will compile with different resulting types and results.

Similar issues arise in conversions between character types (char, char8_t, …) and other types. Character types are provided to represent text in whatever character representation is needed.

void f(char c)
{
  if (c < 0) // may be always false on some platforms.
    {}
}

In addition to the use of strong types (see Type System [IHN]), the implicit conversions and multitude of possible operations of integral types can be mitigated by using scoped enumeration types with the corresponding integer type as its underlying type. For example, std::byte is defined to address individual unsigned char elements (bytes) in memory without participating in arithmetic or bitwise operations.

Because C++ allows function and operator overloading, the effect of implicit conversions provides an additional mechanism of failure, by selecting an unwanted overload during overload resolution due to implicit conversions. This can influence failure modes with lookup as described in section Namespace Issues [BJL]. // Add overload resolution reference!!

C++ also provides a library function std::bit_cast. This function provides the ability to preserve the bit representation when converting between unrelated types. If such is meaningful, then std::bit_cast reduces the risk of some undefined behaviours compared with other type punning approaches such as casts or unions.

6.6.2 Avoidance mechanisms for language users

To avoid the vulnerability or mitigate its ill effects, C++ software developers can: