本文展示一种构造对象的方式,用户无需显式调用构造函数。
对于有参数的构造函数的类,该实现在构造改对象时传递默认值来构造。当然用户也可以指定(绑定)某个参数的值。 实现思路参考boost-ext/di的实现。
来看下例子:
struct Member{
int x = 10;
};
struct Member1 {
int x = 11;
};
class Example1{
public:
Example1(Member x, Member1 x1) {
std::cout << x.x << std::endl; // 10
std::cout << x1.x << std::endl; // 11
}
};
int main() {
auto e1 = farrago::ObjectCreator<>().template Create();
}
例子比较简单,构造一个ObjectCreator对象,并调用他的Create来创建一个Example1的对象,
因为使用ObjectCreator来构造,所以不需要传递参数,它会自动构造。
这样做的好处是,当你构造一个对象时,可以无需考虑这个对象的构造函数是几个参数或类型,当想要增加参数时则无需修改代码,当然指定参数的话除外。这种用法也被称为依赖注入
看起来还蛮酷炫,那主要还是看如何做到的?
先来说下主体想法,首先最重要的当然是ObjectCreator这个类中如何知道要构造的对象的构造函数的参数类型是什么呢,知道参数类型才能构造一个参数传递,同时参数的也同样需要ObjectCreator来构造,依次递归下去。
上边说到了两个问题要解决,第一个就是如何识别构造函数的参数类型,第二个是针对构造函数参数也需要构造的情况下,如果递归构造?
我们使用AnyType的形式来识别出来构造函数的参数,举个简单的例子:
struct AnyType {
template
operator T() {
return T{};
}
};
struct Member {};
struct Example {
Example(Member m, int) {
}
};
int main() {
Example(AnyType(), 2);
return 0;
}
通过调用AnyType()可以匹配至任意类型,然后在构造Example编译器会去找相应的类型来构造。
大家可能发现我使用的是多个参数来举例AnyType,如果参数是一个使用AnyType会有冲突,因为拷贝构造函数也是一个参数,所以编译器会识别冲突,这个问题我们后边也是需要处理的。
class Example {
public:
Example(Member m) {
std::cout << m.x << std::endl;
}
};
int main() {
Example e(AnyType{});
return 0;
}
// -------- 以下报错
note: candidate: 'Example::Example(Member)'
| Example(Member m) {
| ^~~~~~~
: note: candidate: 'constexpr Example::Example(const Example&)'
class Example {
因为构造函数的参数可能是一个类对象,这个对象的构造函数参数又是其他类对象,我们识别类型后继续调用函数来构造这个对象,以此类推。
当然使用过程也不全部是使用默认构造,可能也需要传递特定参数与构造函数的参数进行绑定,但是构造函数的参数类型又是多样的。这里我采用了tuple先来保存,倘若识别出来的类型和保存的数据类型是一致的,则不去构造而是直接传递该数据给构造函数。
那沿着上边的思路就开始写代码,肯定有一个AnyType的类及Objectcreator的类。ObjectCreator用来构造对象返回,会只用AnyType类来识别类型。
大概看下具体的实现:
template
class ObjectCreator {
public:
template
explicit ObjectCreator(Ts&&... args) :
dependency_(std::forward(args)...) {}
// ...
private:
std::tuple dependency_;
};
我们使用tuple保存要绑定的参数时,数据的保存就得进行拷贝,我们这里为了避免拷贝,tuple中的类型是const左引用,这样就得用户自己来维护要绑定的参数的生命周期。
Args是要绑定的参数类型,构造函数中为了避免拷贝使用完美转发来实现。dependency_就是保存绑定参数的数据结构
template
class ObjectCreator {
// ...
template
T Create() {
if constexpr ((std::is_same::value || ...)) {
return std::get(dependency_);
}
else if constexpr (std::is_default_constructible_v) {
return T{};
}
else if constexpr (std::is_constructible>::value) {
return T{AnyFirstRefType{this}};
}
else if constexpr (std::is_constructible>::value) {
return T{AnyFirstType{this}};
}
else {
return CreateMoreParamObject(std::make_index_sequence<10>{});
}
}
// ...
};
这里就是create函数了:
struct AnyType {
template
operator T() {
return T{};
}
template
operator T&() {
return T{};
}
};
class Example {
public:
Example(Member m, int) {
std::cout << m.x << std::endl;
}
};
Example e(AnyType{}, 7);
// 报错如下:
error: conversion from 'AnyType' to 'Member' is ambiguous
Example e(AnyType{}, 7);
^~~~~~~~~
candidate: 'AnyType::operator T() [with T = Member]'
operator T() {
^~~~~~~~
note: candidate: 'AnyType::operator T&() [with T = Member]'
operator T&() {
继续看下多参的构造:
template
T CreateMoreParamObject(const std::index_sequence&) {
if constexpr (std::is_constructible_v, Ns>...>) {
return T{At, Ns>{this}...};
}
else {
return CreateMoreParamObject(std::make_index_sequence{});
}
}
首先判断是否可以由多个AnyRefType类型来构造出来,如果可以的话,直接构造对象,不可以的话就需要将参数个数减少重新匹配。
然后我们来观察AnyType如何编写,先来看下AnyFirstType的情况。
为了避免和拷贝构造函数冲突,简单做一下优化:
struct AnyFirstType {
template >>
constexpr operator T() {
return creator_->template Create();
}
};
我们使用SFINAE来将拷贝构造函数排除在外,使用AnyFirstType识别时参数类型时,需要将要构造的类当作模版参数传递给Src,让T与Src不一样进而告诉编译器要调用的不是拷贝构造函数而是其他的函数。
creator_就是ObjectCreator对象,对参数的构造对Create函数进行递归调用。
多个参数也是类似实现,只是不需要额外判断是不是拷贝构造函数的参数。
不过还有一个点可能需要注意就是,如果构造函数的类型是引用类型,在和绑定参数匹配情况下会多一次拷贝,所以我们也还是区分开来。
template
struct AnyFirstRefType {
template >>,
typename = std::enable_if_t<(std::is_same, Args>::value || ...)>>
constexpr operator T& () {
return const_cast(creator_->template GetDependency());
}
template >>,
typename = std::enable_if_t<(std::is_same, Args>::value || ...)>>
constexpr operator T &&() {
return static_cast(const_cast(creator_->template GetDependency()));
}
Creator* creator_ = nullptr;
};
在和绑定参数匹配并且传递引用的情况下,我们单独实现,直接返回不再调用Creator的Create函数,并且做一下强制转化。多参数的类型识别也是类似。
本文展示了一种对象构造的实现,使用AnyType的思路实现,中间也处理很多的问题。对于无需绑定(或部分绑定)构造函数参数的对象的构造,可扩展性及可维护性都有很好提升。当然该实现目前也尚不完备,目前只是类型绑定,也可以实现参数名字绑定等功能。
上边论述的代码我放到了 https://github.com/leap-ticking/farrago 位置,欢迎取用。