C++中类型和类型转换

本文介绍 C++ 中的类型转换。
部分内容来自C++元编程。隐式转换部分和重载决议有关,需要结合起来看。

涉及 type 的一些概念

Incomplete type

  1. void 以及其 cv-qualified 形式
  2. incompletely-defined object type
    一个 class 被声明(比如一个前向声明),却没有定义。
    bound 未知的数组。
    imcomplete type 构成的数组。
    enum,从它的声明,到它的 underlying type 被确定期间。

这里说明一下,bound 未知的数组未必是 flex 数组。它可能是如下面这种 extern 形式定义的

1
2
3
4
5
extern int x[];      // the type of x is "array of unknown bound of int"
int a[] = {1, 2, 3}; // the type of a is "array of 3 int"

extern int a[][2]; // okay: array of unknown bound of arrays of 2 int
extern int b[2][]; // error: array has incomplete element type

Incomplete type 经常让人头大,需要各种头文件魔法或者 pimpl 来解决。

在下列情况下,需要类型是 Complete 的:

  1. TODO

typd-id

我们可以通过 class/union/enum/typedef/using(type alias) 这些方式定义一个具名的类型。但是在 C++ 中,我们经常使用那些不具名的类型,例如下面的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int* p;               // declaration of a pointer to int
static_cast<int*>(p); // type-id is "int*"

int a[3]; // declaration of an array of 3 int
new int[3]; // type-id is "int[3]" (called new-type-id)

int (*(*x[2])())[3]; // declaration of an array of 2 pointers to functions
// returning pointer to array of 3 int
new (int (*(*[2])())[3]); // type-id is "int (*(*[2])())[3]"

void f(int); // declaration of a function taking int and returning void
std::function<void(int)> x = f; // type template parameter is a type-id "void(int)"
std::function<auto(int) -> void> y = f; // same

std::vector<int> v; // declaration of a vector of int
sizeof(std::vector<int>); // type-id is "std::vector<int>"

struct { int x; } b; // creates a new type and declares an object b of that type
sizeof(struct { int x; }); // error: cannot define new types in a sizeof expression
using t = struct { int x; }; // creates a new type and declares t as an alias of that type

sizeof(static int); // error: storage class specifiers not part of type-specifier-seq
std::function<inline void(int)> f; // error: neither are function specifiers

此外,RTTI 机制还提供了一个 typeid 运算符(不是函数)

1
2
3
4
5
int main() {
assert(typeid(int&) != typeid(int*));
assert(typeid(int) == typeid(int));
return 0;
}

显式类型转换

显式转换有几种类型:

(new-type) expr型。这是 C-style 的,C++ 会按顺序尝试:

  1. const_cast
  2. static_cast(增强的)
    注意,子类的指针或者引用,可以被转成无歧义的基类,即使基类不可访问。对于成员函数的指针也同样适用
  3. static_cast(增强的) + const_cast
  4. reinterpret_cast
  5. reinterpret_cast + const_cast

new-type (expr)型。

需要注意,这种 function-style cast expression 容易和声明产生歧义。此时,这些歧义都会被视作声明。

1
2
3
4
5
6
7
8
9
10
struct M {};
struct L { L(M&); };

M n;
void f()
{
M(m); // declaration, equivalent to M m;
L(n); // ill-formed declaration
L(l)(m); // still a declaration
}

【C++11起】new-type {expr}型。

一个显式类型转换的类型是什么呢?

  1. 对于 lvalue reference,结果是一个 lvalue
  2. 对于函数的 rvalue reference,结果是一个 lvalue
  3. 对于 rvalue reference,结果是一个 xvalue
  4. 对于其他情况,结果是一个 prvalue

Value categories

为了介绍隐式类型转换,介绍Value categories

隐式类型转换

一个隐式类型转换序列包含:

  1. 一系列 standard conversion sequence
  2. 可选的 user-defined conversion
  3. 一系列 standard conversion sequence

When considering the argument to a constructor or to a user-defined conversion function, only a standard conversion sequence is allowed (otherwise user-defined conversions could be effectively chained). When converting from one non-class type to another non-class type, only a standard conversion sequence is allowed.

Standard conversion 的顺序:

  1. zero or one conversion from the following set: lvalue-to-rvalue conversion, array-to-pointer conversion, and function-to-pointer conversion;
  2. zero or one numeric promotion or numeric conversion;
  3. 【C++17】zero or one function pointer conversion;
  4. zero or one qualification conversion.

一个 user-defined conversion 由0或1个单参数的类型转换构造函数,或者一个 non-explicit conversion function call 构成。

An expression e is said to be implicitly convertible to T2 if and only if T2 can be copy-initialized from e, that is the declaration T2 t = e; is well-formed (can be compiled), for some invented temporary t. Note that this is different from direct initialization (T2 t(e)), where explicit constructors and conversion functions would additionally be considered.

decay

首先来看一下什么是decay。例如,对于数组T a[n],除了sizeofalignof、引用限定符&以及字符串常量等少数情形外,a出现时会被decay成指向T的指针。例如下面代码往char s[N]数组中读入了数据。

1
scanf("%s", s);

而很多人会误写为以下的代码

1
scanf("%s", &s);

此时,s的类型实际上是char (*) [N](pointer to an array of char),而scanf希望接受到的是char *(pointer to char)类型。

各种 convertion 类型

Contextual conversions

Case1: 【C++11】从 T 到 bool 的转换。如果 bool t(e) 是 well-formed,也就是说存在 explicit T::operator bool() const;。在下列场景中,这样的表达式 e 会被转换为 bool:

  1. if、while、for 的条件
  2. 逻辑运算符
  3. 三目运算符的条件
  4. static_assert
  5. noexcept
  6. 【C++20】explicit 表达式

Case2: In the following contexts, a context-specific type T is expected, and the expression e of class type E is only allowed if:

  1. 【until C++14】E has a single non-explicit (since C++11) user-defined conversion function to an allowable type.
  2. 【since C++14】对于 E 的所有的 non-explicit conversion functions,如果它们的返回类型是都是 T,或者带 CV 和引用修饰的 T,这样的 e 可以 implicitly convertible to T。

Such expression e is said to be contextually implicitly converted to the specified type T. 【C++11】Note that explicit conversion functions are not considered, even though they are considered in contextual conversions to bool.

  1. the argument of the delete-expression (T is any object pointer type);
  2. integral constant expression, where a literal class is used (T is any integral or unscoped (since C++11) enumeration type, the selected user-defined conversion function must be constexpr);
  3. the controlling expression of the switch statement (T is any integral or enumeration type).

Value transformations

Lvalue-to-rvalue conversion

不考虑旧版本,从 C++11 开始说。
一个非 function 且非 array 类型的 glvalue T 可以被隐式转换为一个 prvalue。此时:

  1. 如果 T 不是 class type, 那么这个 prvalue 的类型是 T,但会去掉 cv 限定符。
  2. 如果 T 是 class type,那么这个 prvalue 的类型是 T。

If an lvalue-to-rvalue conversion from an incomplete type is required by a program, that program is ill-formed.

When an lvalue-to-rvalue conversion is applied to an expression E, the value contained in the referenced object is not accessed if:

  1. E is not potentially evaluated,也就是它是 unevaluated operand,或者 unevaluated operand 中的子表达式。
  2. the evaluation of E results in the evaluation of a member Ex of the set of potential results of E, and Ex names a variable x that is not odr-used by Ex.

Array-to-pointer conversion

An lvalue or rvalue of type “array of N T” or “array of unknown bound of T” can be implicitly converted to a prvalue of type “pointer to T”.
【C++17】If the array is a prvalue, temporary materialization occurs.
The resulting pointer refers to the first element of the array (see array to pointer decay for details)

temporary materialization

A prvalue of any complete type T can be converted to an xvalue of the same type T.
This conversion initializes a temporary object of type T from the prvalue by evaluating the prvalue with the temporary object as its result object, and produces an xvalue denoting the temporary object.

If T is a class or array of class type, it must have an accessible and non-deleted destructor.

1
2
3
struct S { int m; };
int i = S().m; // member access expects glvalue as of C++17;
// S() prvalue is converted to xvalue

Function-to-pointer conversion

Numeric promotions

Integral promotion

Floating-point promotion

Numeric conversions

Integral conversions

Floating-point conversions

Floating–integral conversions

Pointer conversions

Pointer-to-member conversions

Boolean conversions

Qualification conversions

【C++17】Function pointer conversions

重载决议的顺序(rank)

重载决议时,采取下列顺序,可参考 Ranking of implicit conversion sequences 章节。
任何一个 standard conversion sequence 被划分为下面三个 rank:

  1. 精确匹配
    此时,不需要进行任何的转换。
    下面列举出了几种情况:

    1. 无转换
    2. Lvalue-to-rvalue conversion
    3. 【C++17】function pointer conversion
    4. user-defined conversion of class type to the same class
    5. Qualification conversions,即限定符转换
      我们可以为任意类型加上CV限定符。对于多重指针来说,前面的重数的限制要高于后面重数的限制,如
      1
      2
      3
      char** p = 0;
      const char** p1 = p; // error: level 2 more cv-qualified but level 1 is not const
      const char* const * p2 = p; // OK: level 2 more cv-qualified and const added at level 1
  2. Promotion
    即Numeric promotions,包含Integral promotion和Floating-point promotion
    这里注意,非promotion的整数之间转换都作为conversion,如char -> int

  3. Conversion

    1. integral conversion
    2. floating-point conversion
    3. floating-integral conversion
    4. pointer conversion
    5. pointer-to-member conversion
    6. boolean conversion
    7. user-defined conversion of a derived class to its base

The rank of the standard conversion sequence is the worst of the ranks of the standard conversions it holds (there may be up to three conversions)

注意能进行隐式类型转换并不意味着类型相同,所以使用std::is_same进行的判断都是false,例如下面的代码输出都是false。

1
2
std::cout << std::is_same<int, int &>::value << '\n';
std::cout << std::is_same<int, const int &>::value << '\n';

Reference

  1. https://en.cppreference.com/w/cpp/language/explicit_cast
  2. https://en.cppreference.com/w/cpp/language/implicit_conversion
  3. https://en.cppreference.com/w/cpp/language/type