C++17及以前virtual与constexpr混合使用的编译器行为疑问
C++17中virtual与constexpr成员函数混用的标准合规性及相关问题解答
一、virtual与constexpr混用的标准规定
在C17及更早的标准中,明确禁止constexpr成员函数同时被声明为virtual。这是因为constexpr要求函数能够在编译期完成求值,而virtual函数依赖运行期的动态分派(通过虚表查找确定调用的函数版本),二者的设计目标存在本质冲突——编译期无法确定动态分派的目标函数,自然无法满足constexpr的编译期求值要求。C20才正式引入了对constexpr virtual函数的支持,允许在特定条件下结合二者。
二、各编译器行为的原因解析
1. Clang的行为符合标准
Clang严格遵循C++17标准的规定,对constexpr与virtual混用的代码直接报错,这是完全符合标准要求的正确行为。
2. GCC接受代码属于非标准扩展
GCC允许这种写法是其提供的非标准扩展特性,并非标准要求的行为,也不属于编译器bug。GCC在部分场景下会放宽标准限制以提供更灵活的编程体验,但这种写法不具备跨编译器的可移植性。
3. MSVC的报错原因
MSVC的报错提示“无法生成常量表达式”,本质是因为:即使StatSizedObject::GetSize被标记为constexpr,当通过SizedObject基类指针调用时,该调用是动态分派的——编译器无法在编译期确定指针指向的是StatSizedObject还是DynSizedObject实例,也就无法确保该调用能生成编译期常量表达式,因此触发报错。
三、C++17中动态多态结合编译时优化的可行方案
C17没有像C20那样原生支持constexpr virtual,但可以通过以下技巧实现动态多态场景下的编译期优化:
- CRTP静态多态:利用奇异递归模板模式(CRTP)将动态分派转化为编译期的静态分派,结合
constexpr实现编译期求值。这种方式通过模板继承在编译期确定调用的函数版本,避免运行期虚表查找。 - std::variant + std::visit:用
std::variant存储不同的派生类实例,配合std::visit在编译期确定类型并调用对应函数。C++17中std::variant和std::visit已部分支持constexpr,可以在编译期完成类型匹配与函数调用。 - 编译器去虚拟化优化:在某些场景下(比如编译器能确定指针指向的具体类型),编译器会自动对虚函数调用进行去虚拟化优化,将其转化为直接函数调用,从而实现编译期优化。但这依赖编译器的优化能力(如开启-O2及以上优化级别),并非标准保证的行为。
示例代码
#include <cstddef> struct SizedObject { virtual size_t GetSize() const = 0; }; struct DynSizedObject : public SizedObject { size_t s; size_t GetSize() const override final { return s; } }; struct StatSizedObject : public SizedObject { const size_t s; constexpr size_t GetSize() const override final { return s; } constexpr explicit StatSizedObject(const size_t i) : s(i) {} }; int main(int argc, char** argv) { constexpr StatSizedObject SS(42); DynSizedObject DS; DS.s = argc + 2; SizedObject const * p; if (argc > 3) { p = &SS; } else { p = &DS; } return p->GetSize(); }
内容的提问来源于stack exchange,提问作者Oersted
相关产品推荐
相关产品推荐

