我试图使用奇怪的重复模板模式实现静态多态性,当我注意到
static_cast<>
(通常在编译时检查一个类型是否实际上可以转换为另一个类型)错过了基类声明中的拼写错误,从而允许将基类向下转换为其兄弟之一的代码:
#include <iostream>
using namespace std;
template< typename T >
struct CRTP
{
void do_it( )
{
static_cast< T& >( *this ).execute( );
}
};
struct A : CRTP< A >
{
void execute( )
{
cout << "A" << endl;
}
};
struct B : CRTP< B >
{
void execute( )
{
cout << "B" << endl;
}
};
struct C : CRTP< A > // it should be CRTP< C >, but typo mistake
{
void execute( )
{
cout << "C" << endl;
}
};
int main( )
{
A a;
a.do_it( );
B b;
b.do_it( );
C c;
c.do_it( );
return 0;
}
程序的输出是:
A
B
A
为什么演员阵容没有错误?如何进行编译时检查以帮助解决此类错误?
在 CRTP 中解决这个问题的常用方法是让基类有一个私有构造函数,并在模板中声明该类型为友元:
template< typename T >
struct CRTP
{
void do_it( )
{
static_cast< T& >( *this ).execute( );
}
friend T;
private:
CRTP() {};
};
在您的示例中,当您意外地让
C
继承自 CRTP<A>
时,由于 C
不是 CRTP<A>
的友元,因此它无法调用其构造函数,并且由于 C
必须构造其所有基础要构造它自己,你永远无法构造一个C
。唯一的缺点是这不会阻止编译本身;要得到编译器错误,您要么必须尝试实际构造一个 C
,要么为其编写一个用户定义的构造函数。在实践中,这仍然足够好,这样您就不必像其他解决方案所建议的那样在每个派生中添加保护代码(恕我直言,这违背了整个目的)。
实例:http://coliru.stacked-crooked.com/a/38f50494a12dbb54.
注意:根据我的经验,CRTP 的构造函数必须是“用户声明的”,这意味着您不能使用
=default
。否则,在这种情况下,您可以获得聚合初始化,这将不尊重private
。同样,如果您试图保留 trivially_constructible
特征(这不是一个非常重要的特征),这可能是一个问题,但通常这并不重要。
Q1 为什么演员工作没有错误?
当所有明智的事情都不适用时......
来自 https://timsong-cpp.github.io/cppwp/n3337/expr.static.cast#2:
否则,转换的结果是未定义的。
Q2 如何进行编译时检查以帮助解决此类错误?
我找不到可以在
CRTP
中使用的方法。我能想到的最好的方法是在派生类中添加 static_assert
。
例如,如果您将
C
更改为:
struct C : CRTP< A > // it should be CRTP< C >, but typo mistake
{
static_assert(std::is_base_of<CRTP<C>, C>::value, "");
void execute( )
{
cout << "C" << endl;
}
};
您将在编译时看到错误。
您可以将其简化为
struct C : CRTP< A > // it should be CRTP< C >, but typo mistake
{
using ThisType = C;
static_assert(std::is_base_of<CRTP<ThisType>, ThisType>::value, "");
void execute( )
{
cout << "C" << endl;
}
};
每个派生类型都需要添加类似的代码。它并不优雅,但它会起作用。
PS 我不建议使用建议的解决方案。我认为考虑到偶尔的人为错误的开销太大了。
聚会有点晚了,但我们遇到了同样的问题,并采用了类似于 Nir Friedman 的 answer 的解决方案,只不过我们将基本析构函数而不是构造函数设为私有。
主要区别在于它允许将派生类构造函数转发到基本模板。
具体:
template<typename T>
struct CRTP
{
explicit CRTP(/* various arguments */) { /* ... */ }
// ...
private: // enforce correct CRTP use
~CRTP() = default;
friend T;
};
struct Good : CRTP<Good>
{
using CRTP::CRTP; // will not compile if CRTP's ctor(s) is/are private
// ...
};
struct Bad : CRTP<Good> // Compile error
{
// ...
};
除此之外,我们没有发现我们的用法有什么不同。