MSVC 为何允许返回类型由 auto 推导的函数类型别名?
结论
typedef auto T(); 试图定义一个返回类型待推导的函数类型别名,标准 C++ 不允许这种用法。MSVC 接受该示例,应将其视为非标准编译器行为。即使多个版本都接受,也不能证明这是有意提供的扩展或未来的标准功能。
当前工作草案只允许在明确列出的上下文中使用占位类型,未获允许的用法属于 ill-formed。这里的 typedef 声明的是类型别名,既没有声明可从函数体推导返回类型的函数,也没有声明带初始化器的变量。标准草案:占位类型说明符
为什么初始化器 l 不能让这个别名合法
原代码中的这一行本身没有问题:
auto l = +[]{ return 42; };
这个 lambda 不捕获变量,可以转换为函数指针。一元 + 使这里发生转换,因此 l 的类型是 int (*)()。
问题出在随后的别名声明:
typedef auto T();
标准没有规定类型别名可以保留未确定的 auto,再由后续变量的初始化器补全。因此,static inline T* f = l; 不能反过来使之前的别名声明合法。
这涉及两种不同的推导:函数声明中的返回类型占位符从函数体的 return 语句推导;直接用 auto 声明变量时,类型则从变量初始化器推导。标准草案中的推导上下文
是否有相关标准提案或讨论
直接相关的记录有两个 Core Working Group 缺陷报告:
- CWG 1892:Use of auto in function type。它指出早期措辞对函数类型中使用
auto的限制不足,并增加了相应约束。这项修正收紧了规则,不能作为支持待推导函数类型别名的依据。CWG 1892 - CWG 2476:placeholder-type-specifiers and function declarators。它处理了上述限制误伤函数指针变量推导的问题,例如
auto (*fp)() = f;。该修正于 2024 年 3 月作为缺陷报告获接受,允许在适当的变量声明中从初始化器推导类型,但没有建立通过后续使用补全类型别名的机制。CWG 2476
这些记录不能证明 MSVC 正在提前实现这种类型别名特性;现有检索结果也不足以确认该用法已列入未来标准计划。MSVC 的接受行为是有意扩展还是实现缺陷,还需要对应的 Microsoft 文档或缺陷记录来确认。
如何改成可移植的写法
如果只需要保存并调用函数指针,可以直接推导静态成员变量的类型:
auto l = +[]{ return 42; };
struct A {
static inline auto f = l;
};
int main() {
return A::f();
}
如果确实需要函数类型别名,可以从已经确定的函数指针类型中去掉指针:
#include <type_traits>
auto l = +[]{ return 42; };
struct A {
using T = std::remove_pointer_t<decltype(l)>;
static inline T* f = l;
};
int main() {
return A::f();
}
这里 decltype(l) 是 int (*)(),因此 T 是 int()。建立别名时,返回类型已经确定。
注意事项
上述完整示例使用了 inline 静态数据成员,需要 C++17 或更高标准模式。示例中的 42 是 main 的返回值,程序没有向标准输出打印内容。
将原声明简单改成 using T = auto(); 也不能解决标准符合性问题;别名需要指向已经确定的类型。比较编译器行为时,应记录具体版本、标准模式及诊断信息,不能把一次编译成功当作标准支持的证明。
备注:内容仅供参考。
