• 探索一种C++中构造对象的方式


    本文展示一种构造对象的方式,用户无需显式调用构造函数。
    对于有参数的构造函数的类,该实现在构造改对象时传递默认值来构造。当然用户也可以指定(绑定)某个参数的值。 实现思路参考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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    例子比较简单,构造一个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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    通过调用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 {
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    递归构造构造函数的参数

    因为构造函数的参数可能是一个类对象,这个对象的构造函数参数又是其他类对象,我们识别类型后继续调用函数来构造这个对象,以此类推。

    保存绑定参数

    当然使用过程也不全部是使用默认构造,可能也需要传递特定参数与构造函数的参数进行绑定,但是构造函数的参数类型又是多样的。这里我采用了tuple先来保存,倘若识别出来的类型和保存的数据类型是一致的,则不去构造而是直接传递该数据给构造函数。

    代码实现

    那沿着上边的思路就开始写代码,肯定有一个AnyType的类及Objectcreator的类。ObjectCreator用来构造对象返回,会只用AnyType类来识别类型。

    ObjectCreator

    大概看下具体的实现:

    template
    class ObjectCreator {
    public:
        template
        explicit ObjectCreator(Ts&&... args) : 
        	dependency_(std::forward(args)...) {}
    
    // ...
    
    private:
        std::tuple dependency_;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们使用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>{});
        }
    }
    
    // ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    这里就是create函数了:

    • 首先判断是不是要创建的类对象已经绑定了,如果绑定了则直接从tuple中取出返回。
    • 没有绑定的话然后再判断默认构造(即可以无参构造)是否可以构造,可以的话返回一个空对象。
    • 然后进行判断是不是一个参数构造函数的判断,一个参数这里分成了两种,是引用类型或者非引用类型。这样做是因为,T和T&在识别是会冲突,所以分开处理。举例说明:
    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&() {
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 最后是多个参数的构造函数进行构造,一个参数和多个参数分开的原因是,一个参数需要对拷贝构造函数及单参的构造函数冲突的情况进行处理,我们传递了1~10的整数序列作为参数给CreateMoreParamObject函数,这里表示目前该实现最多只能支持10个参数的构造函数。

    继续看下多参的构造:

    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{});
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    首先判断是否可以由多个AnyRefType类型来构造出来,如果可以的话,直接构造对象,不可以的话就需要将参数个数减少重新匹配。

    AnyType

    然后我们来观察AnyType如何编写,先来看下AnyFirstType的情况。
    为了避免和拷贝构造函数冲突,简单做一下优化:

    struct AnyFirstType {
        template >>
        constexpr operator T() {
            return creator_->template Create();
        }
     };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们使用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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在和绑定参数匹配并且传递引用的情况下,我们单独实现,直接返回不再调用Creator的Create函数,并且做一下强制转化。多参数的类型识别也是类似。

    总结

    本文展示了一种对象构造的实现,使用AnyType的思路实现,中间也处理很多的问题。对于无需绑定(或部分绑定)构造函数参数的对象的构造,可扩展性及可维护性都有很好提升。当然该实现目前也尚不完备,目前只是类型绑定,也可以实现参数名字绑定等功能。
    上边论述的代码我放到了 https://github.com/leap-ticking/farrago 位置,欢迎取用。

    ref

    • https://github.com/boost-ext/di
    • https://github.com/leap-ticking/farrago
  • 相关阅读:
    用Python画只三脚猫,不忍直视!
    [matlab]cvx安装后测试代码
    java中多线程去跑海量数据使用线程池批量ThreadPoolExecutor处理的方式和使用Fork/Join框架的方式那种效率高?
    Shell学习--printf命令
    数据结构c版(2)——二叉树
    Java NIO
    实验5 二叉树的应用程序设计
    【路径规划】基于matlab AI抗疫服务移动机器人路径规划系统【含Matlab源码 2096期】
    07.数据持久化之文件操作
    Linux 进程概念
  • 原文地址:https://blog.csdn.net/leapmotion/article/details/133573115