std::is_class 的实现是如何工作的?

How does this implementation of std::is_class work?

我正在尝试了解 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(...);

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

之前定义一个参数

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

参考文献:

What does the T::* mean? Also, is this not a function declaration? It looks like one, yet this compiles without defining a function body.

int T::*pointer to member object。可以按如下方式使用:

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

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

Once again, is this not a function declaration with no body ever defined? Also what does the ellipsis mean in this context?

另一行:

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

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

I would like to understand what this code is doing.

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

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

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

你有两个重载:

  • 仅当 T 是 class 类型时才匹配(在这种情况下,这个是最佳匹配,"wins" 优于第二个)
  • on 每次都匹配

第一个 sizeof 结果产生 1(函数的 return 类型是 char),另一个产生 2(一个结构包含 2 个字符)。

那么检查的布尔值是:

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

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

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

您正在查看的是一种名为 "SFINAE" 的编程技术,它代表 "Substitution failure is not an error"。基本思路是这样的:

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::* 作为参数。它被称为 Member-Pointer 并且是一个指向 int 的指针,但是指向一个 int 那是 class T 的一个成员。这只是一个有效的表达式,如果 T 是一个 class。 第二个是接受任意数量的参数,这在任何情况下都是有效的。

那么它是怎么使用的呢?

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

好的,我们将 0 传递给函数 - 这可以是一个指针,尤其是一个成员指针 - 没有从中获得使用哪个重载的信息。 因此,如果 T 是 class,那么我们可以在此处同时使用 T::*... 重载 - 由于 T::* 重载在这里更为具体,因此它是用过的。 但是如果 T 不是 class,那么我们就不能有类似 T::* 的东西并且重载格式错误。但这是在模板参数替换期间发生的故障。并且由于 "substitution failures are not an error" 编译器将默默地忽略此重载。

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

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

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

表达式 sizeof(detail::test<T>(0)) 在函数调用表达式上使用 sizeof 运算符。 sizeof 的操作数是一个 未评估的上下文 ,这意味着编译器实际上并不执行该代码(即评估它以确定结果)。不必调用该函数来了解 sizeof 结果 是什么 if 您调用它。要知道结果的大小,编译器只需要查看各种 test 函数的声明(了解它们的 return 类型),然后执行重载解析以查看哪个 会被调用,因此要找到 sizeof 结果 是什么。

剩下的谜题是未评估的函数调用detail::test<T>(0)决定了T是否可以用来形成指向成员的指针类型int T::*,这是唯一可能的如果 T 是 class 类型(因为非 class 类型不能有成员,因此不能有指向其成员的指针)。如果 T 是 class 则可以调用第一个 test 重载,否则调用第二个重载。第二个重载使用 printf 风格的 ... 参数列表,这意味着它接受任何东西,但也被认为比任何其他可行的函数更差的匹配(否则使用 ... 的函数也会 "greedy"并一直被调用,即使有一个更具体的函数与参数完全匹配)。在此代码中,... 函数是 "if nothing else matches, call this function" 的回退,因此如果 T 不是 class 类型,则使用回退。

class类型是否真的有一个int类型的成员变量并不重要,对任何class形成int T::*类型都是有效的] (如果该类型没有 int 成员,您就不能使指向成员的指针引用任何成员)。

这里是标准的措辞:

[expr.sizeof]:

The sizeof operator yields the number of bytes occupied by a non-potentially-overlapping object of the type of its operand.

The operand is either an expression, which is an unevaluated operand ([expr.prop])......

2。 [expr.prop]:

In some contexts, unevaluated operands appear ([expr.prim.req], [expr.typeid], [expr.sizeof], [expr.unary.noexcept], [dcl.type.simple], [temp]).

An unevaluated operand is not evaluated.

3。 [temp.fct.spec]:

  1. [Note: Type deduction may fail for the following reasons:

...

(11.7) Attempting to create “pointer to member of T” when T is not a class type. [ Example:

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

— end example ]

如上所示,标准中定义明确:-)

4。 [dcl.meaning]:

[Example:

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;

declares pmi, pmf, pmd and pmc to be a pointer to a member of X of type int, a pointer to a member of X of type void(int), a pointer to a member ofX of type double and a pointer to a member of Y of type char respectively.The declaration of pmd is well-formed even though X has no members of type double. Similarly, the declaration of pmc is well-formed even though Y is an incomplete type.

std::is_class 类型特征是通过编译器内部函数(在大多数流行的编译器上称为 __is_class)表达的,它不能在 "normal" 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);

但是,如果我们将自定义类型特征与前向声明的(尚未定义的)类型一起使用,事情就会变得相当 "interesting":

class ProblemTest;

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

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

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

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

class ProblemTest // definition
{
};

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

成员函数指针的大小增加了三倍!而且即使class 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<ProblemTest> 不会破坏任何大小。

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