我试图了解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(...);
再一次,这不是一个没有定义身体的函数声明吗?省略号在这种情况下的含义是什么?我认为省略号作为函数参数需要在...
之前定义一个参数?
我想了解这段代码在做什么。我知道我可以使用标准库中已经实现的函数,但我想了解它们是如何工作的。
参考文献:
答案 0 :(得分:12)
你正在看的是一些名为&#34; SFINAE&#34;的编程技术。代表&#34;替换失败不是错误&#34;。基本思路是:
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的指针,但是指向类T的成员的int。如果T是类,则它只是一个有效的表达式。
第二个是采用任意数量的论证,无论如何都是有效的。
那么它是如何使用的?
sizeof(detail::test<T>(0))==1
好的,我们将函数传递给0 - 这可以是一个指针,特别是一个成员指针 - 没有从中获取的重载信息。
因此,如果T是一个类,那么我们可以在这里同时使用T::*
和...
重载 - 并且因为T::*
重载是这里更具体的重载,所以使用它。
但是如果T不是一个类,那么我们就不能有类似T::*
的东西,而且重载是不正确的。但它是在模板参数替换期间发生的失败。因为&#34;替换失败不是错误&#34;编译器会默默地忽略这个重载。
之后是sizeof()
已应用。注意到不同的退货类型?因此,根据T
,编译器选择正确的重载,因此选择正确的返回类型,从而导致sizeof(char)
或sizeof(char[2])
的大小。
最后,由于我们只使用此函数的大小而从未实际调用它,因此我们不需要实现。
答案 1 :(得分:12)
到目前为止,其他答案无法解释的令您感到困惑的部分原因是test
函数实际上从未被调用过。他们没有定义的事实并不重要,如果你不打电话给他们。正如您所意识到的那样,整个过程发生在编译时,而不运行任何代码。
表达式sizeof(detail::test<T>(0))
在函数调用表达式上使用sizeof
运算符。 sizeof
的操作数是未评估的上下文,这意味着编译器实际上并不执行该代码(即评估它以确定结果)。调用该函数是不必要的,以便知道sizeof
要知道结果的大小,编译器只需要查看各种test
函数的声明(知道它们的返回类型),然后执行重载解析以查看哪个将成为调用,以便查找结果 的sizeof
。
其余的难题是未评估的函数调用detail::test<T>(0)
确定是否可以使用T
来形成指向成员的类型int T::*
,这只有{ {1}}是一个类类型(因为非类不能拥有成员,因此不能指向其成员)。如果T
是一个类,则可以调用第一个T
重载,否则将调用第二个重载。第二个重载使用test
- 样式...参数列表,这意味着它接受任何东西,但也被认为是比任何其他可行函数更差的匹配(否则使用...的函数也将是#34;贪婪& #34;并且一直被调用,即使有更具体的函数与参数完全匹配)。在此代码中,...函数是&#34的后备;如果没有其他匹配,请调用此函数&#34;,因此如果printf
不是类类型,则使用后备。
如果类类型确实具有T
类型的成员变量并不重要,那么对于任何类来说,无论如何形成类型int
都是有效的(你只是不能&#39;如果类型没有int T::*
成员,则指向成员的指针引用任何成员。
答案 2 :(得分:2)
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,用于定义函数接受任意数量的参数。
我想了解这段代码在做什么。
基本上通过检查struct
是否可以解释为成员指针来检查特定类型是class
还是0
(在这种情况下{{1}是类类型。)
具体来说,在此代码中:
T
你有两个重载:
namespace detail {
template <class T> char test(int T::*);
struct two{
char c[2];
};
template <class T> two test(...);
}
是班级类型时匹配的一个(在这种情况下,这一个是最佳匹配,&#34;胜过&#34;在第二个上)在第一个T
结果产生sizeof
(函数的返回类型为1
),另一个产生2(包含char
个字符的结构)
检查的布尔值是:
2
表示:仅当整数常量sizeof(detail::test<T>(0)) == 1 && !std::is_union<T>::value
可以解释为指向true
类型成员的指针时返回0
,在这种情况下它是类类型的,但它不是T
(这也是一种可能的类型)。
答案 3 :(得分:2)
Test是一个重载函数,它可以在T或任何东西中获取指向成员的指针。 C ++要求使用最佳匹配。因此,如果T是一个类类型,可以在其中有一个成员...然后选择该版本并返回其大小为1.如果T不是类类型,那么T :: *没有意义,因此SFINAE过滤掉了该函数的版本,并且不存在。使用任何版本,它的返回类型大小不是1.因此检查调用该函数返回的大小会导致决定该类型是否可能有成员...只剩下的东西是确保它不是一个联盟来决定如果它是一个班级。
答案 4 :(得分:1)
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<ProblemTest>
不会破坏任何大小。
因此,我的建议是在实现is_class
之类的自定义类型特征时,尽可能依赖编译器内在函数。也就是说,如果您有充分的理由完全手动实现此类类型特征。
答案 5 :(得分:0)
这是标准措辞:
sizeof运算符产生其操作数类型的非潜在重叠对象占用的字节数。
操作数是一个表达式,它是一个未评估的操作数 ([expr.prop])......
在某些情况下,会出现未评估的操作数([expr.prim.req],[expr.typeid],[expr.sizeof],[expr.unary.noexcept],[dcl.type.simple],[temp] )。
未评估未评估的操作数。
- [注意:由于以下原因,类型扣除可能会失败:
醇>...
(11.7)当T不是类类型时,试图创建“指向T成员的指针”。 [例如:
template <class T> int f(int T::*);
int i = f<int>(0);
- 结束的例子 ]
如上所示,它在标准中有明确的定义: - )
[例如:
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是一个,pmc的声明也是格式良好的。不完整的类型。