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

news/2024/5/19 1:10:55 标签: 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<Example1>();
}

例子比较简单,构造一个ObjectCreator对象,并调用他的Create来创建一个Example1的对象,
因为使用ObjectCreator来构造,所以不需要传递参数,它会自动构造。
这样做的好处是,当你构造一个对象时,可以无需考虑这个对象的构造函数是几个参数或类型,当想要增加参数时则无需修改代码,当然指定参数的话除外。这种用法也被称为依赖注入

构思主体实现

看起来还蛮酷炫,那主要还是看如何做到的?
先来说下主体想法,首先最重要的当然是ObjectCreator这个类中如何知道要构造的对象的构造函数的参数类型是什么呢,知道参数类型才能构造一个参数传递,同时参数的也同样需要ObjectCreator来构造,依次递归下去。
上边说到了两个问题要解决,第一个就是如何识别构造函数的参数类型,第二个是针对构造函数参数也需要构造的情况下,如果递归构造?

识别构造函数参数类型

我们使用AnyType的形式来识别出来构造函数的参数,举个简单的例子:

struct AnyType {
    template<typename T>
    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类来识别类型。

ObjectCreator

大概看下具体的实现:

template<typename... Args>
class ObjectCreator {
public:
    template<typename... Ts>
    explicit ObjectCreator(Ts&&... args) : 
    	dependency_(std::forward<Ts>(args)...) {}

// ...

private:
    std::tuple<const Args& ...> dependency_;
};

我们使用tuple保存要绑定的参数时,数据的保存就得进行拷贝,我们这里为了避免拷贝,tuple中的类型是const左引用,这样就得用户自己来维护要绑定的参数的生命周期。
Args是要绑定的参数类型,构造函数中为了避免拷贝使用完美转发来实现。dependency_就是保存绑定参数的数据结构

template<typename... Args>
class ObjectCreator {
// ...

template<typename T>
T Create() {
    if constexpr ((std::is_same<T, Args>::value || ...)) {
        return std::get<const T&>(dependency_);
    }
    else if constexpr (std::is_default_constructible_v<T>) {
        return T{};
    }
    else if constexpr (std::is_constructible<T, 
	    	AnyFirstRefType<ObjectCreator, T, FarragoNull, Args...>>::value) {
        return T{AnyFirstRefType<ObjectCreator, T, FarragoNull, Args...>{this}};
    }
    else if constexpr (std::is_constructible<T, 
    		AnyFirstType<ObjectCreator, T, FarragoNull, Args...>>::value) {
        return T{AnyFirstType<ObjectCreator, T, FarragoNull, Args...>{this}};
    }
    else {
        return CreateMoreParamObject<T>(std::make_index_sequence<10>{});
    }
}

// ...
};

这里就是create函数了:

  • 首先判断是不是要创建的类对象已经绑定了,如果绑定了则直接从tuple中取出返回。
  • 没有绑定的话然后再判断默认构造(即可以无参构造)是否可以构造,可以的话返回一个空对象。
  • 然后进行判断是不是一个参数构造函数的判断,一个参数这里分成了两种,是引用类型或者非引用类型。这样做是因为,T和T&在识别是会冲突,所以分开处理。举例说明:
struct AnyType {
    template<typename T>
    operator T() {
        return T{};
    }

    template<typename T>
    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~10的整数序列作为参数给CreateMoreParamObject函数,这里表示目前该实现最多只能支持10个参数的构造函数

继续看下多参的构造:

template<typename T, std::size_t... Ns>
T CreateMoreParamObject(const std::index_sequence<Ns...>&) {
    if constexpr (std::is_constructible_v<T, 
    		At<AnyRefType<ObjectCreator, FarragoNull, Args...>, Ns>...>) {
        return T{At<AnyRefType<ObjectCreator, FarragoNull, Args...>, Ns>{this}...};
    }
    else {
        return CreateMoreParamObject<T>(std::make_index_sequence<sizeof...(Ns) - 1>{});
    }
}

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

AnyType

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

struct AnyFirstType {
    template <typename T,
    	typename = std::enable_if_t<!std::is_same_v<Src, T>>>
    constexpr operator T() {
        return creator_->template Create<T>();
    }
 };

我们使用SFINAE来将拷贝构造函数排除在外,使用AnyFirstType识别时参数类型时,需要将要构造的类当作模版参数传递给Src,让T与Src不一样进而告诉编译器要调用的不是拷贝构造函数而是其他的函数。
creator_就是ObjectCreator对象,对参数的构造对Create函数进行递归调用。
多个参数也是类似实现,只是不需要额外判断是不是拷贝构造函数的参数。
不过还有一个点可能需要注意就是,如果构造函数的类型是引用类型,在和绑定参数匹配情况下会多一次拷贝,所以我们也还是区分开来。

template <typename Creator, typename Src, typename... Args>
struct AnyFirstRefType {
    template <typename T, 
		typename = std::enable_if_t<!std::is_same_v<Src, std::decay_t<T>>>,
    	typename = std::enable_if_t<(std::is_same<std::decay_t<T>, Args>::value || ...)>>
    constexpr operator T& () {
        return const_cast<T&>(creator_->template GetDependency<T>());
    }

    template <typename T, 
    	typename = std::enable_if_t<!std::is_same_v<Src, std::decay_t<T>>>,
	 	typename = std::enable_if_t<(std::is_same<std::decay_t<T>, Args>::value || ...)>>
    constexpr operator T &&() {
        return static_cast<T&&>(const_cast<T&>(creator_->template GetDependency<T>()));
    }

    Creator* creator_ = nullptr;
};

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

总结

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

ref

  • https://github.com/boost-ext/di
  • https://github.com/leap-ticking/farrago

http://www.niftyadmin.cn/n/5067103.html

相关文章

Docker compose插件安装

添加docker源 # Add Dockers official GPG key: sudo apt-get update sudo apt-get install ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/do…

八大排序算法汇总(C语言实现)

本专栏内容为&#xff1a;八大排序汇总 通过本专栏的深入学习&#xff0c;你可以了解并掌握八大排序以及相关的排序算法。 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;八大排序汇总 &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库…

MySQL存储引擎:选择合适的引擎优化数据库性能

什么是存储引擎&#xff1f; 在MySQL中&#xff0c;存储引擎是数据库管理系统的一部分&#xff0c;负责数据的存储、检索和管理。 常见的MySQL存储引擎 InnoDB InnoDB是MySQL的默认存储引擎&#xff0c;它支持事务和行级锁定&#xff0c;适用于大多数在线事务处理&#xff…

yolov8封装进入ROS系统

一,yolov8的环境配置工作 说明:需要相关工程全部源码的可私聊博主或评论留言 配置工作可以参考我的一篇关于yolov8环境配置的博客。 需要说明的是:直接使用: pip install ultralytics 会导致后面对于网络结构的改进无法运行。所以如果需要对网络结构进行改进的同学,不要使用…

《数字图像处理-OpenCV/Python》连载(10)图像属性与数据类型

《数字图像处理-OpenCV/Python》连载&#xff08;10&#xff09;图像属性与数据类型 本书京东优惠购书链接&#xff1a;https://item.jd.com/14098452.html 本书CSDN独家连载专栏&#xff1a;https://blog.csdn.net/youcans/category_12418787.html 第2章 图像的数据格式 在P…

blender光照系统设置

0&#xff09;Viewport Shading设置里面的Lighting下面的参数&#xff1a; Scene Lights,Scene World - Scene Lights是指在渲染模式下是否使用场景中的灯光对象来照亮物体。 - Scene World是指在渲染模式下是否使用场景中的世界设置来作为背景和环境光。如果关闭该选项&#…

输入为一组4字节的数据,输出为一组5字节的数据加密算法匹配美国通用加密算法

加密算法匹配&#xff1a; * 输入为一组4字节的数据&#xff0c;输出为一组5字节的数据&#xff08;其中第一个字节固定不变&#xff0c;其他4个字节可变&#xff09;&#xff0c; * 匹配所使用的加密算法&#xff0c;可随时提供数据样本。 * 注&#xff1a;相关产品为美国公司…

力扣第102题 广度优先搜索 二叉数 c++

题目 102. 二叉树的层序遍历 中等 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3],[9,20…