CRTP是否没有编译时检查?

9

我尝试使用“奇异递归模板模式”来实现静态多态性,但注意到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

为什么这个程序可以无错误运行?我该如何在编译时检查并避免这种类型的错误?


你可以使用 static_cast 转换到派生类。但你无法在编译时检查运行时类型。 - molbdnilo
@MooingDuck,这很好,但它不能像问题中那样捕获传递错误的派生类型。但是要捕获它,我认为语言中必须支持CRTP(例如mixins)。 - chris
哦,糟糕。你是对的。由于某种原因,我认为其中一个会是C,但它们都是A。也许如果一个变体进入了“派生”类?static_assert(std::is_base_of<CRTP<decltype(this)>,decltype(this)>::value) - Mooing Duck
@MooingDuck,是的,我认为这是今天你能得到的最好结果。使用元类,我相信你可以至少拥有某种形式的 crtp<CRTP> C { ... }; ,而不需要手动继承。也就是说,我认为元类更适合将所需的 mixin 合并到新的元类中或一些通用的 with_mixins<...> Foo { ... }; 中。 - chris
由于AC都是从CRTP<A>派生而来,因此可以从CRTP<A>进行static_cast转换到它们中的任何一个。但是,对于不相关(或模棱两可)的类型,例如B,则需要进行诊断。 - Arne Vogel
2个回答

10
在CRTP中解决这个问题的常规方法是使基类拥有一个私有构造函数,并在模板中声明该类型为友元:
template< typename T >
struct CRTP
{
    void do_it( )
    {
        static_cast< T& >( *this ).execute( );
    }
    friend T;
private:
    CRTP() {};
};

在您的示例中,当您意外地使继承自>时,由于不是>的友元,它无法调用其构造函数,而且由于必须构造所有基类以构造本身,因此您永远无法构造。唯一的缺点是这并不能防止编译本身; 要获得编译器错误,您必须尝试实际构造,或者为其编写用户定义的构造函数。在实践中,这还是足够好的,这样您就不必像另一种解决方案建议的那样在每个派生类中添加保护代码(我认为这有悖于整个目的)。
实时示例:http://coliru.stacked-crooked.com/a/38f50494a12dbb54
注意:根据我的经验,CRTP的构造函数必须“用户声明”,这意味着您不能使用=默认。否则,在这种情况下,您可以获得聚合初始化,它将不尊重私有。同样,如果您试图保持trivially_constructible特性(这不是非常重要的特性),这可能是一个问题,但通常不应该有影响。

这难道不会让人做一些傻事,比如 void MyBadClass::memberFunction() {CRTP<MyBadClass> x; x.do_it();} 吗? - Joshua Green
@JoshuaGreen 好的,构造函数在原始解决方案中是公共的,因此如果您想单独构建CRTP类,则没有任何阻止您这样做。因此,相对于原始情况,我不会说它“允许”您做一些愚蠢的事情。 - Nir Friedman
这很公平。我以为我有一个解决方案,但是我现在想不起来了。 - Joshua Green
@Fan,它应该无法编译,建议重新阅读问题和答案。 - Nir Friedman
1
@Fan 是的,请正确使用模式:Abstract<T>:CRTP <Abstract<T>>。 也就是说,在C ++静态多态性中没有必要有“抽象”类。 - Nir Friedman
显示剩余3条评论

2

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 我不建议使用建议的解决方案。我认为它对偶尔的人为错误来说过于繁琐。


“如何进行编译时检查以避免此类错误?”这确实是问题的关键部分。 - Mooing Duck
@MooingDuck,我复制了您评论中的解决方案。希望您不介意。 - R Sahu
1
我不介意,但是chris指出这是错误的 - Mooing Duck
我应该在哪里写static_assert?我将其添加为do_it()方法的第一行,但尽管有错别字,它仍然编译通过。 - nyarlathotep108
尝试在派生类中使用static_assert(std::is_base_of<CRTP<decltype(this)>,decltype(‌this)>::value) - Mooing Duck
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接