实现与声明分离的库能否受益于 RVO/NRVO?
Can libraries with implementation seperated from decalration benefit from RVO/NRVO?
正如标题所示,我正在编写一个带有 class 模板和多个 non-template 运算符重载的静态库。 class 模板在 a.h 中定义,函数在 a.cc 中定义。
所以我决定继续问 RVO/NRVO 是否可以使库的用户代码受益?
编辑:
很抱歉,这是我刚刚问的另一个问题,不应该放在这个问题中。
为了让前辈更清楚,我实际上是在尝试封装uint8_t等类型,并打算自己写一些大整数类型。
当函数 return 是纯右值时,这意味着(C++17 之前)return 值是一个临时对象。所以,这个临时对象发生了两件事:
- 它必须由创建它的函数初始化。
- 调用创建者的函数必须以某种方式使用它(即使它只是被丢弃,技术上仍在使用它)。
根据标准,如果您执行 return Type(...);
,将计算 return 表达式,生成一个临时文件,该临时文件用于复制初始化 return 值对象.
但是,标准规定如果 Type
与 return 纯右值对象的类型相同,则不必进行此复制初始化。在这种情况下,编译器将直接将临时的初始化应用到 return 值对象。
按照标准,如果您执行 Type var = some_func(...)
,并且 some_func
return 是一个值,那么将使用由 some_func
编辑的临时 return复制初始化 var
.
但是,标准规定如果 Type
是 some_func
return 的值类型,则不必进行复制初始化。这样,some_func
初始化的return值对象就是var
本身。不是一些初始化 var
的临时对象; some_func
直接初始化 var
。
这两个过程完全相互独立。
return值初始化的省略是基于函数的实现。该实现 不关心 调用者在做什么。它只是直接初始化 return 值对象,而不是从 return 表达式进行复制。
从函数的 return 值中省略变量的初始化 不关心 函数的实现。它完全基于以下事实:函数 return 是一个纯右值,并且该纯右值的类型与它所初始化的对象的类型相同。它只需要查看函数的声明即可完成其省略操作的部分。
当这两种情况都发生时,您就可以完全省略从函数内部到最终目的地的所有副本。但两者都不需要对方存在。
因此,请考虑以下几点:
Type foo()
{
Type t;
return t;
}
T t2 = foo();
按照标准,这是两次复制初始化。首先,foo
的 return 值通过从 t
移动来初始化。其次,t2
是通过从 foo
的 return 值移动来初始化的。
如果编译器可以省略这两个,那么你得到 0 步。如果编译器可以省略 t2
的初始化但不对 t
执行 NRVO,那么您将得到 1 次移动。如果两者都做不到,那么您将得到 2 步(并且您应该立即停止使用该编译器;))。
如果您想了解更多实现细节,那么它必须与函数调用约定和 ABI 有关。
函数参数 和 return 值的存储由调用者分配。因此,调用者看到该函数将 return 一个纯右值,因此它分配足够的适当对齐的存储空间来存储该值,然后使用指向该存储空间的指针调用该函数。该函数的实现将在初始化 return 值时使用该存储。
省略,在函数实现方面,只是直接在 return 值内存中构造对象。 Elision,在使用 return 值的函数一侧,只是简单地传入将要使用的对象的存储空间。上面的 t2
示例将通过将 t2
的存储作为 foo
.
的 return 值存储传递来执行省略
foo
的编译器不需要知道或关心 return 值存储是命名值还是临时值。它所知道的是,它已被赋予存储空间,return 值将在其中构造。
foo
的调用者的编译器只需要函数签名,因为它告诉它执行这种省略所需知道的一切。
正如标题所示,我正在编写一个带有 class 模板和多个 non-template 运算符重载的静态库。 class 模板在 a.h 中定义,函数在 a.cc 中定义。
所以我决定继续问 RVO/NRVO 是否可以使库的用户代码受益?
编辑: 很抱歉,这是我刚刚问的另一个问题,不应该放在这个问题中。 为了让前辈更清楚,我实际上是在尝试封装uint8_t等类型,并打算自己写一些大整数类型。
当函数 return 是纯右值时,这意味着(C++17 之前)return 值是一个临时对象。所以,这个临时对象发生了两件事:
- 它必须由创建它的函数初始化。
- 调用创建者的函数必须以某种方式使用它(即使它只是被丢弃,技术上仍在使用它)。
根据标准,如果您执行 return Type(...);
,将计算 return 表达式,生成一个临时文件,该临时文件用于复制初始化 return 值对象.
但是,标准规定如果 Type
与 return 纯右值对象的类型相同,则不必进行此复制初始化。在这种情况下,编译器将直接将临时的初始化应用到 return 值对象。
按照标准,如果您执行 Type var = some_func(...)
,并且 some_func
return 是一个值,那么将使用由 some_func
编辑的临时 return复制初始化 var
.
但是,标准规定如果 Type
是 some_func
return 的值类型,则不必进行复制初始化。这样,some_func
初始化的return值对象就是var
本身。不是一些初始化 var
的临时对象; some_func
直接初始化 var
。
这两个过程完全相互独立。
return值初始化的省略是基于函数的实现。该实现 不关心 调用者在做什么。它只是直接初始化 return 值对象,而不是从 return 表达式进行复制。
从函数的 return 值中省略变量的初始化 不关心 函数的实现。它完全基于以下事实:函数 return 是一个纯右值,并且该纯右值的类型与它所初始化的对象的类型相同。它只需要查看函数的声明即可完成其省略操作的部分。
当这两种情况都发生时,您就可以完全省略从函数内部到最终目的地的所有副本。但两者都不需要对方存在。
因此,请考虑以下几点:
Type foo()
{
Type t;
return t;
}
T t2 = foo();
按照标准,这是两次复制初始化。首先,foo
的 return 值通过从 t
移动来初始化。其次,t2
是通过从 foo
的 return 值移动来初始化的。
如果编译器可以省略这两个,那么你得到 0 步。如果编译器可以省略 t2
的初始化但不对 t
执行 NRVO,那么您将得到 1 次移动。如果两者都做不到,那么您将得到 2 步(并且您应该立即停止使用该编译器;))。
如果您想了解更多实现细节,那么它必须与函数调用约定和 ABI 有关。
函数参数 和 return 值的存储由调用者分配。因此,调用者看到该函数将 return 一个纯右值,因此它分配足够的适当对齐的存储空间来存储该值,然后使用指向该存储空间的指针调用该函数。该函数的实现将在初始化 return 值时使用该存储。
省略,在函数实现方面,只是直接在 return 值内存中构造对象。 Elision,在使用 return 值的函数一侧,只是简单地传入将要使用的对象的存储空间。上面的 t2
示例将通过将 t2
的存储作为 foo
.
foo
的编译器不需要知道或关心 return 值存储是命名值还是临时值。它所知道的是,它已被赋予存储空间,return 值将在其中构造。
foo
的调用者的编译器只需要函数签名,因为它告诉它执行这种省略所需知道的一切。