6.7.2 Object model

1
2
3
4
5
6
7
一个潜在重叠的子对象(`potentially-overlapping subobject`)是: - 一个基类子对象,或; - 一个使用[no_­unique_­address]()特性声明的非静态数据成员

6.7.6 Alignment

1
对象类型具有对齐要求([[basic.fundamental]](), [[basic.compound]]()),这种要求对该类型对象可以被分配到的地址施加限制。对齐量(`alignment`)是一个由实现定义的整数值,它表示某个给定对象在内存中可被分配到的两个连续地址之间相隔的字节数。对象类型会对每个该类型的对象施加一个对齐要求;如果需要更严格的对齐,可以使用对齐说明符(`alignment specifier`)来请求。
2
基本对齐量(`fundamental alignment`)是指一个对齐量小于或等于实现所支持的、在所有上下文中可用的最大对齐量,而这个最大对齐量等于`alignof(std::max_align_t)`(见[[support.types]]())。某个类型在作为完整对象的类型使用时,和其在作为子对象的类型使用时,所需的对齐量可能不同。【示例:
1
2
struct B { long double d; };
struct D : virtual B { char c; };
当D作为完整对象的类型时,它会含有一个类型为B的子对象,因此必须按照`long double`的对齐要求来对齐。但如果D作为另一个对象的子对象出现,而该对象同时也以B作为虚基类,那么B子对象可能属于另一个子对象的一部分,这会降低D子对象本身的对齐要求。——示例结束】 `alignof`运算符的结果反映的是该类型在完整对象情形下的对齐要求。
3
扩展对齐量(`extended alignment`)是指大于`alignof(std::max_align_t)`的对齐量。是否支持扩展对齐量,以及在哪些上下文中支持,均由实现定义(见[[dcl.align]]())。具有扩展对齐要求的类型称为过度对齐类型(`over-aligned type`)。【注释:每一个过度对齐类型都是(或包含)一个应用扩展对齐的类类型(可能是通过一个非静态数据成员引入的)。——注释结束】新扩展对齐量(`new-extended alignment`)是指大于`__STDCPP_DEFAULT_NEW_ALIGNMENT__`的对齐量(见[[cpp.predefined]]())。
4
对齐量以`std::size_t`类型的值来表示。有效的对齐量仅包括:①对基本类型使用`alignof`表达式所返回的值②以及一个由实现定义的附加值集合(该集合可能为空)。每个对齐值都应该是2的非负整数次幂
5
对齐量之间存在从较弱到较强(或更严格)的顺序。更严格的对齐拥有更大的对齐值。一个满足某个对齐要求的地址,同时也满足任何更弱的有效对齐要求。
Paw5zx个人理解:点击查看更多

对齐值越大,对应的要求就越严格

1
2
3
std::cout << std::alignment_of<char>::value << std::endl;   // 1
std::cout << std::alignment_of<int>::value << std::endl; // 4
std::cout << std::alignment_of<double>::value << std::endl; // 8

如果一个地址能满足某个更严格的对齐要求,它也必然满足所有较弱的对齐要求:

  • 地址0x08是8的倍数 → 它可以放double,也可以放int和char
  • 地址0x04是4的倍数 → 它可以放int和char,但不能放double
  • 地址0x01是1的倍数 → 它可以放char,但不能放double和int
6

完整类型的对齐要求可以通过alignof表达式来查询。此外,窄字符类型(narrow character types)必须具有最弱的对齐要求。【注释:这使得普通字符类型能够作为底层类型来管理一个具备对齐要求的内存区域(见[dcl.align])。——注释结束】

7

对齐量之间的比较是有意义的,并且会得到显而易见的结果:

  • (7.1) 当两个对齐量的数值相等时,它们相等。
  • (7.2) 当两个对齐量的数值不相等时,它们不等。
  • (7.3) 当一个对齐量大于另一个时,它表示更严格的对齐。
8
【注释:运行时指针对齐函数([[ptr.align]]())可用于在缓冲区中获得一个对齐的指针;库中的对齐存储模板([[meta.trans.other]]())可用于获得对齐的存储空间。——注释结束】
9
如果在某一特定上下文中请求某个特定的扩展对齐量,而该请求不被实现所支持,那么程序就是非法(`ill-formed`)的。

6.7.7 Temporary objects

1

临时对象在下列情况下被创建:

  • (1.1) 当纯右值被转换为将亡值时([conv.rval]);
  • (1.2) 当实现需要传递或返回平凡可复制类型的对象时(见下文);以及
  • (1.3) 当抛出异常时([except.throw])。【注释:异常对象的生存期在[[except.throw]]中描述。——注释结束】

即使临时对象的创建处于未求值语境([expr.prop]),所有语义限制仍应得到遵守,就如同该临时对象已被创建并随后被销毁了一样(编译阶段假装他会执行,并据此检查所有相关规则)。

【注释:语义限制包括所选构造函数和析构函数的可访问性([class.access])及是否被删除。然而,在decltype说明符([expr.call])操作数这种特殊情形中,不会引入临时a对象,因此上述规定不适用于此类纯右值。——注释结束】

Paw5zx个人理解:点击查看更多

即使临时对象的创建处于未求值语境,所有语义限制仍应得到遵守,就如同该临时对象已被创建并随后被销毁了一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A 
{
public:
A() {}
private:
~A() {} // 私有析构函数
};

int main()
{
// 未求值语境sizeof
// sizeof(A{})会尝试构造一个临时对象A,即使它在sizeof这种不求值环境里
// 标准仍要求按“好像真的构造并销毁了它”来做可访问性检查
// 因为析构函数是private的销毁这个临时对象在当前语境下不被允许,所以编译报错。
// auto size = sizeof(A{}); // 编译报错,无法访问A的析构函数

auto size = sizeof(A()); // ?按道理也是要报错的,但是不知为何编译时不报错

return 0;
}

然而,在decltype说明符操作数这种特殊情形中,不会引入临时对象,因此上述规定不适用于此类纯右值。

1

2

临时对象的物化通常被尽可能延迟,以避免创建不必要的临时对象。【注释:临时对象在下列情况下被物化:

——注释结束】【示例:
考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class X 
{
public:
X(int);
X(const X&);
X& operator=(const X&);
~X();
};

class Y
{
public:
Y(int);
Y(Y&&);
~Y();
};

X f(X);
Y g(Y);

void h()
{
X a(1);
X b = f(X(2));
Y c = g(Y(3));
a = f(a);
}

X(2)直接在f()参数的空间中被构造,Y(3)直接在g()参数的空间中被构造。同样,f()的结果直接构造在b中,g()的结果直接构造在c中。另一方面,表达式a = f(a)需要为f(a)的结果创建一个临时对象,该临时对象被物化以便X​::​operator=(const X&)的引用参数可以绑定到它。——示例结束】

Paw5zx个人理解(针对2.1):点击查看更多

将引用绑定到纯右值时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct X
{
X(int x) : x_(x) {std::cout << "X(" << x_ << ") ctor" << std::endl;; }
X(const X& other) : x_(other.x_) { std::cout << "X(" << x_ << ") copy ctor" << std::endl;; }
X(X&& other) noexcept : x_(other.x_) { std::cout << "X(" << x_ << ") move ctor" << std::endl;; }
~X() { std::cout << "~X(" << x_ << ")" << std::endl;; }
int x_;
};

void test_2_1()
{
const X& ref = X(42);
}

Paw5zx个人理解(针对2.2):点击查看更多

对类类型的纯右值进行成员访问时

1
2
3
4
5
void test_2_2()
{
int n = X(7).x_;
X(8).~X();
}
Paw5zx个人理解(针对2.3):点击查看更多
1

Paw5zx个人理解(针对2.4):点击查看更多
1

Paw5zx个人理解(针对2.5):点击查看更多
1

Paw5zx个人理解(针对2.6):点击查看更多
1

Paw5zx个人理解:点击查看更多

X(2)直接在f()参数的空间中被构造

自C++17起,X(2)不会先创建一个临时对象再拷贝给参数,而是直接在参数对象的存储空间中构造。(copy elision)

f()的结果直接构造在b

同样返回值也一样被优化,返回值直接构造在变量b的存储空间中

表达式a = f(a)需要为f(a)的结果创建一个临时对象,该临时对象被物化以便X​::​operator=(const X&)的引用参数可以绑定到它。

【【以下未完成分析,缺少知识点:copy elision,更详细的是NRVO以及延申知识点:临时物化,临时对象(又回来了),主要弄清,临时对象是个什么状态以及是否真的在内存中创建(构造)这么个对象。所以先完成余下部分的学习

对于a = f(a),情况有些不同,为了演示,我们补全class Xf(X)g(Y)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class X 
{
public:
X(int){std::cout << "X(int) ctor" << std::endl;};
X(const X&){std::cout << "X copy ctor" << std::endl;};
X& operator=(const X&){ std::cout << "X copy assignment" << std::endl; return *this; };
~X(){std::cout << "X dtor" << std::endl;};
};

X f(X param)
{
X local(param);
return local; // 触发NRVO
}

// g()随意补充,为了过编译
Y g(Y param)
{
return Y(4);
}

现在对于:

1
2
X a(1);
a = f(a);
  • ①首先构造a,调用的是X(int)
  • ②然后是f(X)的实参传递,由于a是左值,按值传递,所以调用一次拷贝构造,把a拷贝至param
  • ③现在进入f(X),用param拷贝构造local
  • ④在f(X)的返回语句中,NRVO生效,因此没有从local到返回对象的构造,而是直接 由于axxx,所以将local赋值给a,调用拷贝赋值
  • f(X)结束,析构临时对象local以及形参param,析构顺序这里不讨论
  • ⑥析构a

所以输出为:

1
2
3
4
5
6
7
X(int) ctor
X copy ctor
X copy ctor
X copy assignment
X dtor
X dtor
X dtor
3

当类类型X的对象被传递给函数或从函数返回时,如果X至少有一个可用的(eligible)拷贝构造函数或移动构造函数([special]),且每个这样的构造函数都是平凡的,并且X的析构函数要么是平凡的要么是被删除的,则允许实现创建一个临时对象用来保存函数形参或结果对象。

该临时对象从函数实参(或返回值表达式)构造,然后函数形参(或返回对象)被初始化,就好像是通过一个可用的平凡的构造函数,从该临时对象拷贝初始化而来一样。(即使该构造函数不可访问,或者不会被重载决议选择来执行对象的复制或移动)。【注释:这一自由度旨在允许在寄存器中传递或返回类类型的对象。——注释结束】

Paw5zx个人理解:点击查看更多

允许(非强制)编译器把满足一定条件的“简单类对象”放在寄存器中传递或返回,而不是一定要真正在栈上放一个完整的对象再调用拷贝构造,从而提高效率。

编译器只有在满足以下条件时才可以做这种特殊处理:

  • X有至少一个可用的拷贝或移动构造函数(即编译器认为可以用它来拷贝/移动对象)
  • 每个这样的构造函数都是平凡的(即可以按位拷贝,不需要运行用户代码)
  • X的析构函数要么是平凡的要么是被删除的(不需要运行用户代码)

临时对象的创建方式是:

  • ①编译器先根据实参(或返回值表达式)创建一个临时对象

即使该构造函数是:xxxxx

编译器仍可以xxxx,行为就好像xxxx(但实际没有)

4
5
6
7
8