【问题标题】:How to detect existence of a class using SFINAE?如何使用 SFINAE 检测类的存在?
【发布时间】:2012-05-29 12:42:43
【问题描述】:

是否可以使用SFINAE 检测 C++ 中是否存在类?如果可以的话怎么做?

假设我们有一个仅由某些版本的库提供的类。我想知道是否可以使用 SFINAE 来检测该类是否存在。检测结果是任意的,比如说一个枚举常量,如果存在则为 1,否则为 0。

【问题讨论】:

  • 您的意思是检测一个类是否包含特定类型?或者如果一个类型存在于命名空间范围内? (我看不出后者有什么用)
  • 编译器会为你做这件事。请说明你想做什么?
  • 澄清一下——类只是一个硬编码的标识符,所以它根本不依赖于模板参数替换?
  • @sbabbi:命名空间范围内的类。作为类似配置的构建步骤的替换/添加,它非常有用。
  • @dirkgently:在理想情况下,是的,这可以通过检查版本宏或类似的东西来处理。不幸的是,图书馆设计者并不总是提供这种信息。

标签: c++ templates sfinae


【解决方案1】:

如果我们要求编译器告诉我们关于类类型 T 的任何信息 即使被声明,我们也一定会得到一个编译错误。没有办法 周围。因此,如果我们想知道T 类是否“存在”,T 可能还没有声明,我们必须先声明T

但这没关系,因为仅仅声明T 不会让它“存在”,因为 T 存在 必须是指T 已定义。如果,声明T, 然后你可以确定它是否已经定义,你不需要在 任何混乱。

所以问题是判断T是否是一个已定义的类类型。

sizeof(T) 在这里没有帮助。如果T 未定义,那么它将给出一个 incomplete type T 错误。同样typeid(T)。也没什么好处 在 T * 类型上制作 SFINAE 探针,因为 T * 已定义类型 只要 T 已声明,即使 T 未声明。既然我们是 必须声明类Tstd::is_class<T> 不是 要么回答,因为该声明足以让它说“是”。

C++11 在<type_traits> 中提供了std::is_constructible<T ...Args>。能 这提供了一个现成的解决方案? - 如果定义了T,那么它必须至少有一个构造函数。

恐怕不会。如果您知道至少一位公众的签名 T 的构造函数,然后 GCC 的 <type_traits>(截至 4.6.3)确实可以 这生意。假设一个已知的公共构造函数是T::T(int)。那么:

std::is_constructible<T,int>::value

如果定义了T,则为真,如果仅声明T,则为假。

但这不是便携式的。 VC++ 2010 中的&lt;type_traits&gt; 还没有提供std::is_constructible,如果T 没有定义,甚至它的std::has_trivial_constructor&lt;T&gt; 也会失败:很可能当std::is_constructible 到达时它会效仿。此外,如果只有 T 的私有构造函数存在用于提供给 std::is_constructible 然后甚至是 GCC 会吐(这是挑眉)。

如果定义了T,它必须有一个析构函数,并且只有一个析构函数。并且该析构函数比T 的任何其他可能成员更可能公开。有鉴于此,我们能做的最简单、最强大的游戏就是制作一个 SFINAE 探针来检测T::~T 的存在。

此 SFINAE 探针无法以常规方式制作,用于确定 T 是否具有普通成员函数 mf - 使“是重载” SFINAE 探测函数的参数采用术语定义的参数 &amp;T::mf的类型。因为我们不允许获取地址 析构函数(或构造函数)。

尽管如此,如果定义了T,那么T::~T 的类型为DT- 必须是 由decltype(dt) 产生,只要dt 是一个计算结果为 调用T::~T;因此DT * 也将是一种类型,可以在 原则作为函数重载的参数类型给出。因此我们 可以这样写探针(GCC 4.6.3):

#ifndef HAS_DESTRUCTOR_H
#define HAS_DESTRUCTOR_H

#include <type_traits>

/*! The template `has_destructor<T>` exports a
    boolean constant `value that is true iff `T` has 
    a public destructor.

    N.B. A compile error will occur if T has non-public destructor.
*/ 
template< typename T>
struct has_destructor
{   
    /* Has destructor :) */
    template <typename A> 
    static std::true_type test(decltype(std::declval<A>().~A()) *) {
        return std::true_type();
    }

    /* Has no destructor :( */
    template<typename A>
    static std::false_type test(...) {
        return std::false_type(); 
    }

    /* This will be either `std::true_type` or `std::false_type` */
    typedef decltype(test<T>(0)) type;

    static const bool value = type::value; /* Which is it? */
};

#endif // EOF

只有T 必须有一个public 析构函数才能在decltype(std::declval&lt;A&gt;().~A()) 的参数表达式中合法调用。 (has_destructor&lt;T&gt; 是我贡献的方法自省模板的简化改编 here。)

参数表达式std::declval&lt;A&gt;().~A() 的含义可能对某些人来说是模糊的,特别是std::declval&lt;A&gt;()。函数模板std::declval&lt;T&gt;()&lt;type_traits&gt; 中定义并返回T&amp;&amp;(对T 的右值引用)——尽管它只能在未计算的上下文中调用,例如decltype 的参数。所以std::declval&lt;A&gt;().~A() 的含义是在给定A 上调用~A()std::declval&lt;A&gt;() 在这里为我们提供了很好的服务,因为它不需要T 的任何公共构造函数,或者让我们知道它。

因此,“Yes 重载”的 SFINAE 探针的参数类型是:指向 A 的析构函数类型的指针test&lt;T&gt;(0) 将匹配该重载以防万一有这样一种类型作为A的析构函数,对于A = T

有了has_destructor&lt;T&gt;——并且牢记它对T的公开可破坏值的限制——你可以通过确保你声明来测试一个类T是否在你的代码中的某个点被定义 在问问题之前。这是一个测试程序。

#include "has_destructor.h"
#include <iostream>

class bar {}; // Defined
template< 
    class CharT, 
    class Traits
> class basic_iostream; //Defined
template<typename T>
struct vector; //Undefined
class foo; // Undefined

int main()
{
    std::cout << has_destructor<bar>::value << std::endl;
    std::cout << has_destructor<std::basic_iostream<char>>::value 
        << std::endl;
    std::cout << has_destructor<foo>::value << std::endl;
    std::cout << has_destructor<vector<int>>::value << std::endl;
    std::cout << has_destructor<int>::value << std::endl;
    std::count << std::has_trivial_destructor<int>::value << std::endl;
    return 0;
}

使用 GCC 4.6.3 构建,这将告诉您 2 个 // Defined 类 有析构函数,而 2 个 // Undefined 类没有。第五 输出行会说 int 是可破坏的,最后 行将显示std::has_trivial_destructor&lt;int&gt; 同意。如果我们想要 将字段缩小到类类型,std::is_class&lt;T&gt; 可以在之后应用 我们确定T 是可破坏的。

Visual C++ 2010 不提供std::declval()。支持该编译器 您可以在has_destructor.h 的顶部添加以下内容:

#ifdef _MSC_VER
namespace std {
template <typename T>
typename add_rvalue_reference<T>::type declval();
}
#endif

【讨论】:

  • 这确实是一个很好的分析。我认为它实际上等同于我提出的解决方案,只是它使用析构函数作为钩子而不需要特殊的转换构造函数。有一个权衡 - 这个解决方案将在大多数时间透明地工作,但我的将一直工作,代价是在每个类定义中调用一个宏。
  • 嗨,很好的答案,我正在尝试修改您的代码以使用类模板,到目前为止,我通过将 'typename A/T' 更改为 'template class A/T' 以匹配我模板的参数,但即使有公共析构函数,测试也会默默失败。你知道为什么会这样吗?我知道我可以将模板实例化为一个类并使用原始模板,但是我在专门化模板之前在宏中使用了这个检查,所以我不能在专门化之前实例化它。谢谢!
  • 没有看到你的代码就不可能说出来。使解决方案适应类模板对我来说很简单,所以你的代码有问题。我建议你用合适的标题问一个新问题,然后说:“我试图使 适应类模板,但没有成功。这是我的代码......”
  • 我试图让它工作,但不能——“真正的”重载永远不会出现,即,总是被 SFINAE 消除。这是我发现的:delctype(…) 中的类型,即析构函数的返回类型,是void &amp;。并且不可能创建指向它的指针。这是我的 GCC 告诉我的:cannot declare pointer to 'using DestructorReturnType = void&amp;' {aka 'void&amp;'}.
  • 经过进一步调查,这是GCC 9.1中的一个错误:gcc.gnu.org/bugzilla/show_bug.cgi?id=90598#c0所有其他编译器都应该没问题。
【解决方案2】:

在这篇文章中仍然没有找到满意的答案...

Mike Kinghan 开始答对了,说了一件聪明的事:

所以问题是判断T是否是一个已定义的类类型。

但是

sizeof(T) 在这里没有帮助

不正确...

下面是使用sizeof(T) 的方法:

template <class T, class Enable = void>
struct is_defined
{
    static constexpr bool value = false;
};

template <class T>
struct is_defined<T, std::enable_if_t<(sizeof(T) > 0)>>
{
    static constexpr bool value = true;
};

【讨论】:

  • 这个答案很容易理解,并且适用于 GCC 7.2.1 和 VS 2017。而评分最高的答案在 GCC 上给了我“不完整类型”错误。对此表示赞赏:)
  • 从标准 "sizeof 运算符不得应用于具有函数或不完整类型的表达式,应用于其基础类型在其所有枚举数被声明之前未固定的枚举类型,应用于带括号的名称此类类型,或指定位域的泛左值。”
  • @IsaacPascual 是的,这正是它起作用的原因:当类型不完整时,第二次替换失败。实际上,这可以重命名为is_complete,但就类而言,它只是is_defined。顺便说一句,一年后我刚刚意识到,sizeof(T) &gt; 0 可以简单地替换为sizeof(T)
【解决方案3】:

对于 SFINAE,没有。我认为名称查找技巧是完成此任务的方法。如果您不害怕将名称注入库的命名空间:

namespace lib {
#if DEFINE_A
class A;
#endif
}

namespace {
    struct local_tag;
    using A = local_tag;
}

namespace lib {
    template <typename T = void>
    A is_a_defined();
}

constexpr bool A_is_defined =
  !std::is_same<local_tag, decltype(lib::is_a_defined())>::value;

Demo.

如果在全局命名空间中声明了A

#if DEFINE_A
class A;
#endif

namespace {
    struct local_tag;
    using A = local_tag;
}

namespace foo {
    template <typename T = void>
    ::A is_a_defined();
}

constexpr bool A_is_defined =
  !std::is_same<local_tag, decltype(foo::is_a_defined())>::value;

Demo.

【讨论】:

    【解决方案4】:

    好的,我想我找到了一种方法来做到这一点,尽管可能有更好的方法。假设我们有类 A,它包含在库的某些实例中,而不包含在其他实例中。诀窍是在 A 中定义一个特殊的私有转换构造函数,然后使用 SFINAE 来检测转换构造函数。当包含A时,检测成功;如果不是,则检测失败。

    这是一个具体的例子。首先是检测模板的header,class_defined.hpp:

    struct class_defined_helper { };
    
    template< typename T >
    struct class_defined {
    
      typedef char yes;
      typedef long no;
    
      static yes test( T const & );
      static no  test( ... );
    
      enum { value = sizeof( test( class_defined_helper( )) == sizeof( yes ) };
    };
    
    #define CLASS_DEFINED_CHECK( type )     \
      type( class_defined_helper const & ); \
                                            \
      friend struct class_defined< type >;
    

    现在是一个包含类定义的头文件,blah.hpp:

    #include "class_defined.hpp"
    
    #ifdef INCLUDE_BLAH
    class blah {
      CLASS_DEFINED_CHECK( blah );
    };
    #else
    class blah;
    #endif
    

    现在是源文件,main.cpp:

    #include "blah.hpp"
    
    int main( ) {
      std::cout << class_defined< blah >::value << std::endl;
    }
    

    使用定义的 BLAH_INCLUDED 编译会打印 1。如果没有定义 BLAH_INCLUDED,则会打印 0。不幸的是,这仍然需要类的前向声明才能在两种情况下编译。我没有办法避免这种情况。

    【讨论】:

    • 这并不真正检测类型是否定义(甚至声明),而是检测是否定义了宏。在这种情况下,删除所有代码并将其保留为#if CLASS_DEFINED_MACRO,然后是依赖代码可能会更简单。
    • 我不关注。关键是不可能检查一个类型是否被声明,因为为了检查你是否需要声明它。我认为除非你有一个钩子可以在类型中查找,否则也无法确定是否定义了一个类型。该宏以转换构造函数的形式提供该挂钩。我的解决方案允许您编译带有和不带有类定义的库,并检查该类是否使用 SFINAE 定义。这与我认为我们可以达到 OP 想要的一样接近。许多模式需要类型中的钩子。这就是我们这里的所有宏。
    • 除非它与SFINAE无关。
    猜你喜欢
    • 2013-02-20
    • 2020-05-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多