为什么 return 类型的 C++ 函数模板实例化包含在损坏的函数名称中?
Why is the return type of C++ function template instantiations included in the mangled function name?
Itanium ABI specifies,除了几个无趣的例外,return 类型包含在模板实例的错位名称中,但不包含在非模板中。
这是为什么?在什么情况下您可以有两个函数模板实例化,链接器需要区分它们,因为它不表示违反单一定义规则或类似情况?
举例说明我的意思:
class ReturnType {};
class ParamType {};
template <typename T>
ReturnType foo(T p) {
return ReturnType();
};
template ReturnType foo<ParamType>(ParamType);
ReturnType bar(ParamType p) {
return ReturnType();
}
然后生成的目标文件有重整:
ReturnType foo<ParamType>(ParamType)
=> _Z3fooI9ParamTypeE10ReturnTypeT_
^^^^^^^^^^^^
ReturnType bar(ParamType)
=> _Z3bar9ParamType
为什么 foo
需要 ReturnType
损坏但 bar
不需要?
(我假设这是有原因的,这不仅仅是一个随意的选择。)
可能是因为与普通函数不同,函数模板签名包含 return 类型? §1.3:
1.3.17 signature <
function>
name, parameter type list (8.3.5), and enclosing namespace (if any)
[ Note: Signatures are used as a
basis for name mangling and linking. — end note ]
1.3.18 signature <
function template>
name, parameter type list (8.3.5), enclosing namespace (if any), return
type, and template parameter list
考虑一下我们可以有两个完全不同的函数模板重载,它们仅在 return 类型上不同,如果这样写的话:
template <int>
char foo();
template <int>
int foo();
如果名称修改不考虑 return 类型,那么链接这些模板将很困难,因为 foo<0>
不会唯一地命名一个专业化。不过,可以使用重载解析(不带参数)来解决一种专业化问题:
int (*funptr)() = foo<0>;
另一方面,普通函数不需要包含 return 类型,因为它们不能在它们的 return 类型上重载 - 即它们的签名不包含 return类型。
模板函数可以通过 return 类型单独重载,这与常规函数不同。
template <typename T> int f() { return 1; }
template <typename T> long f() { return 2; }
int main() {
int (&f1) () = f<void>;
long (&f2) () = f<void>;
return f1() == f2();
}
此处,假设一个非优化编译器,生成的程序集将包含两个函数 f<void>()
,但它们不能共享相同的错位名称,否则生成的程序集将无法用于 main
来指定它指的是哪个实例化。
通常情况下,如果你有一个重载的模板函数,只有一个定义将用于特定的模板参数,所以这并不常见,但在对 Columbo 的回答的评论中,dyp 提出了基本的想法这实际上可能有什么用。在Can addressof() be implemented as constexpr function?中,我想到了
template <bool>
struct addressof_impl;
template <>
struct addressof_impl<false> {
template <typename T>
static constexpr T *impl(T &t) {
return &t;
}
};
template <>
struct addressof_impl<true> {
template <typename T>
static /* not constexpr */ T *impl(T &t) {
return reinterpret_cast<T *>(&const_cast<char &>(reinterpret_cast<const volatile char &>(t)));
}
};
template <typename T>
constexpr T *addressof(T &t)
{
return addressof_impl<has_overloaded_addressof_operator<T>::value>::template impl<T>(t);
}
但如果在多个翻译单元中使用相同的实例化 addressof<X>
,这实际上是 ODR 违规,其中一些 X
是不完整的,而一些 X
是完整的并且具有重载的 &
运算符。这可以通过使用常规重载函数直接执行 addressof
中的逻辑来重新工作。
template <typename T>
std::enable_if_t<has_overloaded_addressof_operator<T>::value, T *>
addressof(T &t)
{
return reinterpret_cast<T *>(&const_cast<char &>(reinterpret_cast<const volatile char &>(t)));
}
template <typename T>
constexpr
std::enable_if_t<!has_overloaded_addressof_operator<T>::value, T *>
addressof(T &t)
{
return &t;
}
(出于同样的原因,has_overloaded_addressof_operator
也需要内联。)
这样就避免了问题:当 X
不完整时,addressof<X>
指的是与 X
完整时不同的函数。
Itanium ABI specifies,除了几个无趣的例外,return 类型包含在模板实例的错位名称中,但不包含在非模板中。
这是为什么?在什么情况下您可以有两个函数模板实例化,链接器需要区分它们,因为它不表示违反单一定义规则或类似情况?
举例说明我的意思:
class ReturnType {};
class ParamType {};
template <typename T>
ReturnType foo(T p) {
return ReturnType();
};
template ReturnType foo<ParamType>(ParamType);
ReturnType bar(ParamType p) {
return ReturnType();
}
然后生成的目标文件有重整:
ReturnType foo<ParamType>(ParamType)
=> _Z3fooI9ParamTypeE10ReturnTypeT_
^^^^^^^^^^^^
ReturnType bar(ParamType)
=> _Z3bar9ParamType
为什么 foo
需要 ReturnType
损坏但 bar
不需要?
(我假设这是有原因的,这不仅仅是一个随意的选择。)
可能是因为与普通函数不同,函数模板签名包含 return 类型? §1.3:
1.3.17 signature
<
function>
name, parameter type list (8.3.5), and enclosing namespace (if any)
[ Note: Signatures are used as a basis for name mangling and linking. — end note ]
1.3.18 signature<
function template>
name, parameter type list (8.3.5), enclosing namespace (if any), return type, and template parameter list
考虑一下我们可以有两个完全不同的函数模板重载,它们仅在 return 类型上不同,如果这样写的话:
template <int>
char foo();
template <int>
int foo();
如果名称修改不考虑 return 类型,那么链接这些模板将很困难,因为 foo<0>
不会唯一地命名一个专业化。不过,可以使用重载解析(不带参数)来解决一种专业化问题:
int (*funptr)() = foo<0>;
另一方面,普通函数不需要包含 return 类型,因为它们不能在它们的 return 类型上重载 - 即它们的签名不包含 return类型。
模板函数可以通过 return 类型单独重载,这与常规函数不同。
template <typename T> int f() { return 1; }
template <typename T> long f() { return 2; }
int main() {
int (&f1) () = f<void>;
long (&f2) () = f<void>;
return f1() == f2();
}
此处,假设一个非优化编译器,生成的程序集将包含两个函数 f<void>()
,但它们不能共享相同的错位名称,否则生成的程序集将无法用于 main
来指定它指的是哪个实例化。
通常情况下,如果你有一个重载的模板函数,只有一个定义将用于特定的模板参数,所以这并不常见,但在对 Columbo 的回答的评论中,dyp 提出了基本的想法这实际上可能有什么用。在Can addressof() be implemented as constexpr function?中,我想到了
template <bool>
struct addressof_impl;
template <>
struct addressof_impl<false> {
template <typename T>
static constexpr T *impl(T &t) {
return &t;
}
};
template <>
struct addressof_impl<true> {
template <typename T>
static /* not constexpr */ T *impl(T &t) {
return reinterpret_cast<T *>(&const_cast<char &>(reinterpret_cast<const volatile char &>(t)));
}
};
template <typename T>
constexpr T *addressof(T &t)
{
return addressof_impl<has_overloaded_addressof_operator<T>::value>::template impl<T>(t);
}
但如果在多个翻译单元中使用相同的实例化 addressof<X>
,这实际上是 ODR 违规,其中一些 X
是不完整的,而一些 X
是完整的并且具有重载的 &
运算符。这可以通过使用常规重载函数直接执行 addressof
中的逻辑来重新工作。
template <typename T>
std::enable_if_t<has_overloaded_addressof_operator<T>::value, T *>
addressof(T &t)
{
return reinterpret_cast<T *>(&const_cast<char &>(reinterpret_cast<const volatile char &>(t)));
}
template <typename T>
constexpr
std::enable_if_t<!has_overloaded_addressof_operator<T>::value, T *>
addressof(T &t)
{
return &t;
}
(出于同样的原因,has_overloaded_addressof_operator
也需要内联。)
这样就避免了问题:当 X
不完整时,addressof<X>
指的是与 X
完整时不同的函数。