用成员函数重载实现is_default_constructible
首先介绍一个C++标准库提供的可变参类模板std::is_default_constructible。这个类模板的主要功能是判断一个类的对象是否能被默认构造(所谓默认构造,就是构造一个类对象时,不需要给该类的构造函数传递任何参数)。例如,有一个类A和一个类B,代码如下。
#include "killCmake.h"
#include<string>
using namespace std;
class A {
};
class B
{
public:
B(int tmpval) {
}
};
int main()
{
A a_obj;
// E0291: 类 "B" 不存在默认构造函数
B b_obj;
// 要构造类B对象,必须给类B的构造函数提供一个参数
B b_obj(1);
return 0;
}
现在,可以使用std::is_default_constructible判断类A和类B对象是否能被默认构造(该类没有构造函数或有一个不带参数的构造函数)。在main()主函数中添加代码:
#include "killCmake.h"
#include<string>
using namespace std;
class A {
};
class B
{
public:
B(int tmpval) {
}
};
int main()
{
std::cout << std::is_default_constructible<int>::value << std::endl;
std::cout << std::is_default_constructible<double>::value << std::endl;
std::cout << std::is_default_constructible<A>::value << std::endl;
std::cout << std::is_default_constructible<B>::value << std::endl;
return 0;
}
-
从结果中可以看到,int、double等基本类型(内部类型)对象以及类A对象都是可以默认构造的(结果为1),而类B对象因为其构造函数带一个形参(该形参没有默认值),所以无法默认构造。
-
在明白了std::is_default_constructible的功能后,现在就来深入了解一下它的实现源码。
-
如果让你来实现一个与
std::is_default_constructible
同样的功能,借此看一看如何使用SFINAE特性萃取一些重要信息(这里要萃取的信息是判断某个类是否“没有构造函数或有一个不带参数的构造函数”,满足这个条件的类就能够默认构造)。IsDefConstructible类模板的代码如下。
template<typename T>
class IsDefConstructible
{
private:
template<typename = decltype(T())>
static std::true_type test(void*);
template<typename = int>
static std::false_type test(...);
public:
static constexpr bool value = IsSameType<decltype(test(nullptr)), std::true_type>::value;
};
- (1)有两个同名的静态成员函数模板test()。注意观察,第1个test()的返回类型是std::true_type,而第2个test()的返回类型是std::false_type。第1个test()的形参是void*,而第2个test()的形参是3个点(…),这个形参读者应该不陌生,是C语言中的省略号形参,代表它可以接受0到任意多个实参。
尤其要注意第1个和第2个test()的模板参数,都有默认值,第1个test()的模板默认值比较关键(decltype(T())),要重点留意。两个test()都只有声明而没有实现体,因为做类型推断一类事物(一般涉及decltype)的时候往往不需要具体的实现。 - (2)对于这两个test()静态成员函数(重载函数),调用的时候,编译器会优先选择有具体形参的test()版本,只有该test()版本不匹配时才会选择带省略号形参的test()版本(带省略号的形参具有最低的匹配优先级)。换句话说,优先匹配第1个test()版本,只有第1个test()版本不匹配时,才会去匹配第2个test()版本。
- (3)最关键的是静态成员变量value的取值,value的最终取值是一个布尔值true(1)或false(0)。如果value最终取值为1,就表示通过模板参数传递给IsDefConstructible的类对象能默认构造,如果value最终取值为0,就表示通过模板参数传递给IsDefConstructible的类对象不能默认构造。
- 看一看value的最终取值是经过怎样的计算得到的,也就是重点分析下面这行代码:
IsSameType< decltype(test(nullptr)), std::true_type>::value;
- 上面这行代码用前面讲过的IsSameType<…>::value判断decltype(test(nullptr))和std::true_type这两个类型是否相等,如果相等就返回true,否则返回false。
- 重点就是代码段decltype(test(nullptr)),这段代码利用decltype判断test()函数的返回类型:如果传递给IsDefConstructible的类型T支持默认构造,那么显然编译器会选择第1个test()并通过decltype推导出返回类型为std::true_type,从而使IsSameType<…>::value返回true;如果传递给IsDefConstructible的类型T不支持默认构造,那么第1个test()就不会成立(因其类型模板参数的默认值不支持类型T的默认构造导致decltype(T())的写法根本就不成立),根据SFINAE特性,编译器会选择第2个test(),然后通过decltype推导出返回类型为std::false_type,从而使IsSameType<…>::value返回false。
- 以上就是IsDefConstructible类模板的实现细节。现在,可以对main()主函数的代码行进行修改,测试IsDefConstructible类模板能否正常工作。
#include "killCmake.h"
#include<string>
using namespace std;
// 泛化版本
template<typename T1,typename T2>
struct IsSameType
{
static const bool value = false;
};
// 特化版本
template<typename T1>
struct IsSameType<T1, T1>
{
static const bool value = true;
};
class A {
};
class B
{
public:
B(int tmpval) {
}
};
template<typename T>
class IsDefConstructible
{
private:
template<typename = decltype(T())>
static std::true_type test(void*);
template<typename = int>
static std::false_type test(...);
public:
static constexpr bool value = IsSameType<decltype(test(nullptr)), std::true_type>::value;
};
int main()
{
std::cout << IsDefConstructible<int>::value << std::endl;
std::cout << IsDefConstructible<double>::value << std::endl;
std::cout << IsDefConstructible<A>::value << std::endl;
std::cout << IsDefConstructible<B>::value << std::endl;
return 0;
}
- 一切正常。这种技术所对应的代码的核心特点就是使用了两个同名但不同返回值的重载成员函数模板。
- 当然,实现代码不止上面这一种,换一种写法供读者参考,让IsDefConstructible作为辅助类并且改个名字叫作IsDefConstructibleHelper,代码如下。
template<typename T>
class IsDefConstructibleHelper
{
private:
template<typename = decltype(T())>
static std::true_type test(void*);
template<typename = int>
static std::false_type test(...);
public:
using type = decltype(test(nullptr));
};
- 上面代码的意思比较好理解,使用using定义了别名type,结合以往的IsDefConstructible类模板不难理解:如果类型T支持默认构造,那么type将为std::true_type类型,否则将为std::false_type类型。现在,改写以往的IsDefConstructible类模板,让其继承刚定义的IsDefConstructibleHelper模板中的type(type是一个类型,为std::true_type或std::false_type),代码如下。
template<typename T>
class IsDefConstructible : public IsDefConstructibleHelper<T>::type
{
};
- 上面的代码并不难理解,分析一下,当类型T支持默认构造时,IsDefConstructible的父类(IsDefConstructibleHelper::type)就相当于std::true_type类型;当类型T不支持默认构造时,IsDefConstructible的父类就相当于std::false_type类型。此时, std::true_type或std::false_type类型中的静态成员变量value的值为true(1)或false(0)就分别代表类型T支持或不支持默认构造。
- main()主函数中的代码不需要做任何调整,运行程序,结果没有发生变化,一切正常。
#include "killCmake.h"
#include<string>
using namespace std;
// 泛化版本
template<typename T1,typename T2>
struct IsSameType
{
static const bool value = false;
};
// 特化版本
template<typename T1>
struct IsSameType<T1, T1>
{
static const bool value = true;
};
class A {
};
class B
{
public:
B(int tmpval) {
}
};
template<typename T>
class IsDefConstructibleHelper
{
private:
template<typename = decltype(T())>
static std::true_type test(void*);
template<typename = int>
static std::false_type test(...);
public:
using type = decltype(test(nullptr));
};
template<typename T>
class IsDefConstructible : public IsDefConstructibleHelper<T>::type
{
};
int main()
{
std::cout << IsDefConstructible<int>::value << std::endl;
std::cout << IsDefConstructible<double>::value << std::endl;
std::cout << IsDefConstructible<A>::value << std::endl;
std::cout << IsDefConstructible<B>::value << std::endl;
return 0;
}