[英]convert member function to pointer to member function
Clang,GCC,MSVC对成员函数的转换有不同的看法。 谁是对的?
https://gcc.godbolt.org/z/QNsgwd
template<typename T>
struct a
{
template <typename... Args>
void va(Args...) {}
template <typename X>
void x(X) {}
void y(int) {}
};
struct b : a<b>
{
void testva()
{
using F = void (a<b>::*)();
F f = (F)&a<b>::va<int>; // gcc: error, msvc: error, clang: ok
}
void testx()
{
using F = void (a<b>::*)();
F f = (F)&a<b>::x<int>;// gcc: error, msvc: ok, clang: ok
}
void testy()
{
using F = void (a<b>::*)();
F f = (F)& a<b>::y; // gcc: ok, msvc: ok, clang: ok
}
};
testx
和testy
格式正确,因此gcc对testx
是错误的。 但该标准对于testva
有些模糊。
用最简单的开始,在testy
表达&a<b>::y
名称的非模板函数,它不过载,所以它有键入void (a<b>::*)(int)
,而不需要进一步的分析。 从任何指向成员函数的指针转换到任何其他指向成员函数的指针都是格式正确的reinterpret_cast
其中包含未指定的结果,除非转换回原始类型,并且C样式转换可以执行reinterpret_cast
可以执行的操作做。
对于模板函数,我们有[over.over] / 1-2 :
在某些上下文中,使用不带参数的重载函数名称解析为函数,指向函数的指针或指向来自重载集的特定函数的成员函数的指针。 函数模板名称被认为是在这种上下文中命名一组重载函数。 与A类型功能
F
被选择用于功能类型FT
在上下文需要,如果目标类型的F
(后可能将所述函数指针转换)是相同的FT
。 目标可以是
...
显式类型转换([expr.type.conv],[expr.static.cast],[expr.cast]),
...
如果名称是函数模板,则完成模板参数推导([temp.deduct.funcaddr]),如果参数推导成功,则生成的模板参数列表用于生成单个函数模板特化,并将其添加到考虑重载函数集。 [ 注意:如[temp.arg.explicit]中所述,如果演绎失败且函数模板名称后跟显式模板参数列表,则会检查template-id以查看它是否标识单个函数模板特化。 如果是,则模板ID被认为是该功能模板特化的左值。 目标类型不用于该确定。 - 结束说明 ]
所以这意味着我们首先尝试对a<b>::x<int>
进行模板参数推导,将其与目标类型void (a<b>::*)()
相匹配。 但是没有专业化可以给出完全匹配,因为它们都有一个参数,而不是零,因此演绎失败。 但是根据说明,还有[temp.arg.explicit](C ++ 17中的第3段, 最新的C ++ 20草案中的4 ):
可以从显式模板参数列表中省略可以从默认模板参数推导或获得的尾随模板参数。 不以其他方式推导出的尾随模板参数包([temp.variadic])将被推断为模板参数的空序列。 ...在演绎完成和失败的上下文中,或在未进行演绎的上下文中,如果指定了模板参数列表,并且它与任何默认模板参数一起标识单个函数模板特化,则模板ID是函数模板特化的左值。
在testx
, template-id a<b>::x<int>
标识单个函数模板特化。 因此它命名为专门化,并且C样式转换也是有效的,具有未指定的结果。
那么在testva
, a<b>::va<int>
确定了一个专业化? 通过[temp.arg.explicit] / 9 ,肯定可以使用该表达式来命名不同的特化 :
模板参数推导可以扩展与模板参数包对应的模板参数序列,即使序列包含显式指定的模板参数也是如此。
除了这个说“模板参数演绎”。 这里涉及的模板参数推导失败,因为它需要与目标类型void (a<b>::*)()
进行不可能的匹配。 因此没有真正解释a<b>::va<int>
标识单个特化,因为没有描述获取其他模板参数的其他方法,或者标识多个特化,因为它可以在具有匹配目标类型的其他上下文中有效使用。
[expr.reinterpret.cast]/10
如果
T1
和T2
都是函数类型或两种对象类型,则可以将类型为“T1
类型X
的成员的指针”的prvalue显式转换为不同类型的prvalue“指向类型为T2
的Y
的成员的指针”。 空成员指针值将转换为目标类型的空成员指针值。 除以下情况外,此转换的结果未指定:
- 将“指向成员函数的指针”类型的prvalue转换为不同的指向成员函数类型的指针并返回其原始类型会生成原始指向成员的值。
- 转换类型的prvalue“指针的数据成员
X
型的T1
”的类型“指针的数据成员Y
类型的T2
”(其中的对准要求T2
并不比那些更严格的T1
),并返回到其原始type产生原始指向成员的值。
&a<b>::va<int>
等。 是“类型为void(int)
类型a<b>
成员的指针”类型的prvalue并且转换它(不调用其值未指定的结果函数指针)是合法的。
让我们简化一下这个例子:
struct a {
template <typename... Args>
void va(Args...) {}
template <typename X>
void x(X) {}
void y(int) {}
};
using no_args = void(a::*)();
using int_arg = void(a::*)(int);
让我们尝试以下四件事:
reinterpret_cast<no_args>(&MEMBER_FUNCTION); // (1)
(no_args) &MEMBER_FUNCTION; // (2)
(no_args) static_cast<int_arg>(&MEMBER_FUNCTION); // (3)
int_arg temp = &MEMBER_FUNCTION; (no_args) temp; // (4)
(用&a::va<int>
, &a::x<int>
和&a::y
替换MEMBER_FUNCTION
)。
clang编译所有这些。
gcc使用&a::va<int>
和&a::x<int>
编译除(2)之外的所有内容。
MSVC使用&a::va<int>
编译除(1)和(2)之外的所有内容(但是&a::x<int>
可以很好)。
请注意,(3)与(4)基本相同。
https://gcc.godbolt.org/z/a2qqyo显示了这样的一个例子。
您可以从中看到的是,在模板的情况下, &MEMBER_FUNCTION
没有解析为特定的成员函数指针,但如果它被解析,则允许将其重新解释为另一个成员函数指针类型。
标准有什么要说的:
在某些上下文中,使用不带参数的重载函数名称解析为函数,指向函数的指针或指向来自重载集的特定函数的成员函数的指针。 函数模板名称被认为是在这种上下文中命名一组重载函数。 如果F(在可能应用函数指针转换之后)与FT相同,则为上下文中所需的目标类型的函数类型FT选择类型为F的函数。 [注意:也就是说,当匹配指向成员函数的指针类型时,忽略函数所属的类。 - 结束说明]目标可以是:
[...]
- 显式类型转换([expr.type.conv],[expr.static.cast],[expr.cast])
稍后给出的一个例子是:
int f(double);
int f(int);
void g() {
(int (*)(int))&f; // cast expression as selector
}
还有一些关于模板的引用:
在获取重载函数的地址时,可以从指定的类型推导出模板参数。 函数模板的函数类型和指定的类型用作P和A的类型,并且如[temp.deduct.type]中所述完成演绎。
[...]如果指定了模板参数列表,并且它与任何默认模板参数一起标识单个函数模板特化,那么template-id是函数模板特化的左值。
这看起来像MSVC是对的。
除非您将其分配/转换为void(a::*)(int)
否则不会解析&a::va<int>
。 您还应该能够将它分配给void(a::*)(int, char)
或void(a::*)(int, double, char)
,其中Args
将被推导为{ int, char }
和{ int, double, char }
。 这意味着(no_args) &a::va<int>
应该失败,因为它可能有许多可能的Args
集(所有这些都以int
开头,而clang过分地解析它),并且它们都没有采用零参数, so (no_args) &a::va<int>
是一个应该失败的static_cast
。
至于&a::x<int>
,只有一个可能的函数,所以它应该和&a::y
工作方式完全相同(但是gcc还没有解决它)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.