【问题标题】:How does this implementation of std::is_class work?std::is_class 的这种实现是如何工作的?
【发布时间】:2016-05-14 19:27:44
【问题描述】:

我正在尝试了解std::is_class 的实现。我复制了一些可能的实现并编译了它们,希望弄清楚它们是如何工作的。完成后,我发现所有的计算都是在编译过程中完成的(我应该早点发现,回头看),所以 gdb 不能给我更多关于到底发生了什么的细节。

我很难理解的实现是这个:

template<class T, T v>
    struct integral_constant{
    static constexpr T value = v;
    typedef T value_type;
    typedef integral_constant type;
    constexpr operator value_type() const noexcept {
        return value;
    }
};

namespace detail {
    template <class T> char test(int T::*);   //this line
    struct two{
        char c[2];
    };
    template <class T> two test(...);         //this line
}

//Not concerned about the is_union<T> implementation right now
template <class T>
struct is_class : std::integral_constant<bool, sizeof(detail::test<T>(0))==1 
                                                   && !std::is_union<T>::value> {};

我在使用两条注释行时遇到了问题。第一行:

 template<class T> char test(int T::*);

T::* 是什么意思?另外,这不是函数声明吗?它看起来像一个,但它在没有定义函数体的情况下编译。

我要理解的第二行是:

template<class T> two test(...);

再一次,这不是没有定义主体的函数声明吗?在这种情况下,省略号是什么意思?我认为省略号作为函数参数需要在 ...? 之前定义一个参数?

我想了解这段代码在做什么。我知道我可以使用标准库中已经实现的函数,但我想了解它们是如何工作的。

参考资料:

【问题讨论】:

  • 函数声明不包含正文。这是定义。
  • 函数定义是函数声明,Karoly。您选择的要点是所有定义都是声明,但并非所有声明都是定义。

标签: c++ templates c++11 implementation c++-standard-library


【解决方案1】:

T::* 是什么意思?另外,这不是函数声明吗?它看起来像一个,但它在没有定义函数体的情况下编译。

int T::*pointer to member object。可以这样使用:

struct T { int x; }
int main() {
    int T::* ptr = &T::x;

    T a {123};
    a.*ptr = 0;
}

再一次,这不是没有定义主体的函数声明吗?在这种情况下,省略号是什么意思?

在另一行:

template<class T> two test(...);

省略号 is a C construct 定义函数接受任意数量的参数。

我想了解这段代码在做什么。

基本上,它通过检查0 是否可以解释为成员指针(在这种情况下T 是类类型)来检查特定类型是struct 还是class

具体来说,在这段代码中:

namespace detail {
    template <class T> char test(int T::*);
    struct two{
        char c[2];
    };
    template <class T> two test(...);
}

你有两个重载:

  • 仅当 T 是类类型时才匹配(在这种情况下,这是最佳匹配并“胜过”第二个)
  • 每次都匹配上

在第一个sizeof 中,结果产生1(函数的返回类型为char),另一个产生2(包含2 字符的结构)。

然后检查的布尔值是:

sizeof(detail::test<T>(0)) == 1 && !std::is_union<T>::value

这意味着:仅当整数常量 0 可以解释为指向 T 类型成员的指针时才返回 true(在这种情况下,它是类类型),但它不是 union(它也是一种可能的类类型)。

【讨论】:

  • "其他产生 2" 至少 2
  • 为什么至少有两个?
  • 通常正好是 2,但sizeof(struct S) 何时保证具有精确值?
【解决方案2】:

Test 是一个重载函数,它接受一个指向 T 中成员的指针或任何东西。 C++ 要求使用最佳匹配。因此,如果 T 是一个类类型,它可以在其中包含一个成员...然后选择该版本并且其返回的大小为 1。如果 T 不是一个类类型,则 T::*零意义,这样函数的版本就会被 SFINAE 过滤掉并且不会在那里。使用任何版本,它的返回类型大小不是 1。因此检查调用该函数的返回大小会导致决定该类型是否可能有成员......唯一剩下的就是确保它不是一个联合来决定是否是班级。

【讨论】:

    【解决方案3】:

    您正在查看的是一种称为“SFINAE”的编程技术,它代表“替换失败不是错误”。基本思路是这样的:

    namespace detail {
      template <class T> char test(int T::*);   //this line
      struct two{
        char c[2];
      };
      template <class T> two test(...);         //this line
    }
    

    此命名空间为 test() 提供了 2 个重载。两者都是模板,在编译时解析。第一个以int T::* 作为参数。它被称为成员指针,是一个指向 int 的指针,但指向一个 int 是类 T 的成员。这只是一个有效的表达式,如果 T 是一个类。 第二个是接受任意数量的参数,这在任何情况下都是有效的。

    那么它是如何使用的呢?

    sizeof(detail::test<T>(0))==1
    

    好的,我们向函数传递一个 0 - 这可以是一个指针,尤其是一个成员指针 - 没有从中获得使用哪个重载的信息。 因此,如果 T 是一个类,那么我们可以在这里同时使用 T::*... 重载 - 由于 T::* 重载在这里更具体,因此使用它。 但是如果 T 不是一个类,那么我们就不能拥有像 T::* 这样的东西,并且重载是格式错误的。但它是在模板参数替换期间发生的失败。而且由于“替换失败不是错误”,编译器会默默地忽略这个重载。

    之后是sizeof() 应用。注意到不同的返回类型了吗?因此,根据T,编译器会选择正确的重载,从而选择正确的返回类型,从而产生sizeof(char)sizeof(char[2]) 的大小。

    最后,由于我们只使用这个函数的大小而从未真正调用它,所以我们不需要实现。

    【讨论】:

    • “更具体”是什么意思?
    • @curiousguy:T::* 重载只能用于可转换为成员指针的表达式。 ... 重载可用于任何表达式。
    • 这是一个标准术语吗?
    • 我认为“专业化”不是“过载”,而是更准确的词。对于函数模板,类型参数是从参数推导出来的。在您的特定示例中,选择在 test(int T::*) 和 test(...) 之间。
    【解决方案4】:

    到目前为止,其他答案尚未解释的部分让您感到困惑的是 test 函数实际上从未被调用过。如果您不调用它们,它们没有定义的事实并不重要。正如您所意识到的,整个事情发生在编译时,无需运行任何代码。

    表达式sizeof(detail::test&lt;T&gt;(0)) 在函数调用表达式上使用sizeof 运算符。 sizeof 的操作数是一个未评估的上下文,这意味着编译器实际上并不执行该代码(即评估它以确定结果)。没有必要调用该函数来了解sizeof 的结果would 如果 你调用它。要知道结果的大小,编译器只需要查看各种test 函数的声明(了解它们的返回类型),然后执行重载决议以查看将调用哪个,所以要找到sizeof 的结果是什么。

    剩下的谜题是未求值的函数调用detail::test&lt;T&gt;(0) 确定T 是否可用于形成指向成员类型int T::*,这只有在T 是类类型时才有可能(因为非类不能有成员,因此不能有指向其成员的指针)。如果T 是一个类,则可以调用第一个test 重载,否则调用第二个重载。第二个重载使用printf-style ... 参数列表,这意味着它可以接受任何内容,但也被认为比任何其他可行的函数匹配更差(否则使用 ... 的函数将过于“贪婪”并被调用 all时间,即使有一个更具体的函数与参数完全匹配)。在此代码中,... 函数是“如果没有其他匹配项,则调用此函数”的后备,因此如果 T 不是类类型,则使用后备。

    类类型是否真的有一个int类型的成员变量并不重要,对于任何类来说,形成int T::*类型是有效的(你只是不能让那个指向成员的指针如果类型没有int 成员,则引用任何成员)。

    【讨论】:

    • 最后三行加 1。
    【解决方案5】:

    这是标准措辞:

    [expr.sizeof]:

    sizeof 运算符产生其操作数类型的非潜在重叠对象占用的字节数。

    操作数是一个表达式,它是一个未计算的操作数 ([expr.prop])......

    2。 [expr.prop]:

    在某些情况下,会出现未计算的操作数([expr.prim.req]、[expr.typeid]、[expr.sizeof]、[expr.unary.noexcept]、[dcl.type.simple]、[temp] )。

    未计算的操作数不会被计算。

    3。 [temp.fct.spec]:

    1. [注:类型推导可能失败,原因如下:

    ...

    (11.7) 当 T 不是类类型时,尝试创建“指向 T 成员的指针”。 [ 例子:

      template <class T> int f(int T::*);
      int i = f<int>(0);
    

    ——结束示例 ]

    如上所示,它在标准中是明确定义的:-)

    4。 [dcl.含义]:

    [示例:

    struct X {
    void f(int);
    int a;
    };
    struct Y;
    
    int X::* pmi = &X::a;
    void (X::* pmf)(int) = &X::f;
    double X::* pmd;
    char Y::* pmc;
    

    将 pmi、pmf、pmd 和 pmc 声明为指向 int 类型的 X 成员的指针、指向 void(int) 类型的 X 成员的指针、指向 double 类型的 X 成员的指针和指针分别指向 char 类型的 Y 的成员。即使 X 没有 double 类型的成员,pmd 的声明也是格式良好的。 同样,即使 Y 是类型不完整。

    【讨论】:

      【解决方案6】:

      std::is_class 类型特征通过编译器内在函数(在大多数流行的编译器上称为 __is_class)表示,它不能在“普通”C++ 中实现。

      std::is_class 的那些手动 C++ 实现可用于教育目的,但不能用于实际生产代码。否则,前向声明的类型可能会发生不好的事情(std::is_class 也应该可以正常工作)。

      这是一个可以在任何 msvc x64 编译器上重现的示例。

      假设我已经编写了自己的is_class 实现:

      namespace detail
      {
          template<typename T>
          constexpr char test_my_bad_is_class_call(int T::*) { return {}; }
      
          struct two { char _[2]; };
      
          template<typename T>
          constexpr two test_my_bad_is_class_call(...) { return {}; }
      }
      
      template<typename T>
      struct my_bad_is_class
          : std::bool_constant<sizeof(detail::test_my_bad_is_class_call<T>(nullptr)) == 1>
      {
      };
      

      让我们试试吧:

      class Test
      {
      };
      
      static_assert(my_bad_is_class<Test>::value == true);
      static_assert(my_bad_is_class<const Test>::value == true);
      
      static_assert(my_bad_is_class<Test&>::value == false);
      static_assert(my_bad_is_class<Test*>::value == false);
      static_assert(my_bad_is_class<int>::value == false);
      static_assert(my_bad_is_class<void>::value == false);
      

      只要T 类型在第一次应用my_bad_is_class 时完全定义,一切都会好起来的。并且其成员函数指针的大小将保持应有的大小:

      // 8 is the default for such simple classes on msvc x64
      static_assert(sizeof(void(Test::*)()) == 8);
      

      但是,如果我们将自定义类型特征与前向声明(尚未定义)的类型一起使用,事情就会变得非常“有趣”:

      class ProblemTest;
      

      以下行隐式请求类型 int ProblemTest::* 用于前向声明的类,编译器现在无法看到其定义。

      static_assert(my_bad_is_class<ProblemTest>::value == true);
      

      这会编译,但意外地破坏了成员函数指针的大小。

      编译器似乎在我们请求类型的同时尝试“实例化”(类似于模板的实例化方式)指向ProblemTest 的成员函数的指针的大小int ProblemTest::* 在我们的 my_bad_is_class 实现中。而且,目前,编译器不知道它应该是什么,因此它别无选择,只能假设最大可能的大小。

      class ProblemTest // definition
      {
      };
      
      // 24 BYTES INSTEAD OF 8, CARL!
      static_assert(sizeof(void(ProblemTest::*)()) == 24);
      

      成员函数指针的大小增加了三倍!并且即使在编译器看到了类ProblemTest的定义后,它也无法缩回。

      如果您使用一些第三方库,这些库依赖于编译器上特定大小的成员函数指针(例如,Don Clugston 著名的 FastDelegate),由于调用类型特征可能是一种真正的痛苦。主要是因为类型特征调用不应该修改任何内容,但在这种特殊情况下,它们会修改 - 即使对于经验丰富的开发人员来说,这也是非常出乎意料的。

      另一方面,如果我们使用 __is_class 内部函数实现了 is_class,一切都会好起来的:

      template<typename T>
      struct my_good_is_class
          : std::bool_constant<__is_class(T)>
      {
      };
      
      class ProblemTest;
      
      static_assert(my_good_is_class<ProblemTest>::value == true);
      
      class ProblemTest
      {
      };
      
      static_assert(sizeof(void(ProblemTest::*)()) == 8);
      

      在这种情况下,my_good_is_class&lt;ProblemTest&gt; 的调用不会破坏任何大小。

      因此,我的建议是在实现您的自定义类型特征(如 is_class)时尽可能依赖编译器内在函数。也就是说,如果您有充分的理由手动实现此类类型特征。

      【讨论】:

      • 这真是一个有趣的发现。这表明我们应该尽可能使用 void_t 习惯用法(而不是函数重载)来使用成员指针进行检测。看这个godbolt例子:godbolt.org/z/7M8qo3,你可以改变BAD_IS_CLASS_TYPE宏值来检查不同的行为。
      • 看来,当 MSVC 实例化 detail::test_call(int T::*) 的调用约定时,T 的一些额外信息也会被实例化——包括 T 的成员函数指针类型及其尺寸
      猜你喜欢
      • 1970-01-01
      • 2014-12-18
      • 2011-05-30
      • 1970-01-01
      • 2020-03-13
      • 2011-12-15
      • 1970-01-01
      • 2015-01-11
      • 1970-01-01
      相关资源
      最近更新 更多