首页 > 编程语言 >C++ 中 Concept-Model 概念模型

C++ 中 Concept-Model 概念模型

时间:2023-06-14 21:12:56浏览次数:49  
标签:std task 概念模型 process self Concept Model unique void

此文档参考自:https://gracicot.github.io/conceptmodel/2017/09/13/concept-model-part1.html ,觉得很有趣,就翻译过来了

一、Concept-Model:多态的新视角

面向对象编程大家都很熟悉,只需实现一个接口 Interface 。但这种使用经典 OOP 实现的多态性是侵入性的,即使在真正不需要的地方也会强制多态行为,比如总会触发堆分配,伴随着一个包含基类的列表。今天我们要介绍一种新的概念模型( Runtime ConceptVirtual Concept ),这是一种可能会改变你对多态性及其试图解决的问题的看法的模型。
让我们从一个简单的例子开始:样例中有一个接口 abstract_task ,一个或多个实现(即 print_task ),一个类型擦除列表 tasks,以多态方式执行该 process 函数。

// Our interface.
struct abstract_task {
    virtual void process() = 0;
    virtual ~abstract_task() = default;
};

// An implementation of our interface.
struct print_task : abstract_task {
    void process() override {
        std::cout << "Print Task" << std::endl;
    }
};

// A type erased list of tasks.
std::vector<std::unique_ptr<abstract_task>> tasks;

// A function that push a new task in the list.
void push(std::unique_ptr<abstract_task> task) {
    tasks.emplace_back(std::move(task));
}

// execute all tasks and clear the list.
void run() {
    for(auto&& task : tasks) {
        task->process();
    }
    
    tasks.clear();
}

上面的代码符合我们大部分的编程直觉。首先,这里需要动态分配,且没有办法解决它。但真实意图不是“我们想要 100% 的时间进行动态分配!” ,真实意图是“我们想要一个类型擦除的任务列表”。然而,始终通过动态分配和多态恰好是最常见的方式,而且它也是语言自动实现多态的唯一方式。
其次,它不适用于所有类,很多人可能会说:

是的,只需实现该接口即可!所有类型都有效!

问题是,并非所有类型都可以继承 abstract_task 。假设有这样一个类:

struct some_library_task : library_task {
    void process() { /* ... */ }
};

且要求你不能更改该类,你必须实现一个 Adaptor 才能使其在代码中工作。
此外,还有另一种类不可能扩展接口:lambdas 。是的,lambda !你的代码可能与他们不兼容!想象一下写这样的东西:

push([] { std::cout << "print something!"; });

遗憾的是,这行不通,因为 lambda 不是动态分配的,也不会扩展类 abstract_task
Concept-Model 惯用法旨在解决这些问题,我们来具体是怎么实现的。

二、Concept-Model: The adapter pattern on steroids

在本节中,我们将解释从经典 OOP 到 Concept-Model 的迁移过程。将会分解为许多小步骤,以便更容易理解。
首先,函数 push 接受一个 std::unique_ptr。想象一下,假如你有几十个函数以这种方式执行任务,如果有一天你需要所有那些采用 std::unique_ptr<abstract_task> 原始指针或引用的函数怎么办?我们先从这一点入手:取而代之的是包含指针的结构:

struct task {
    task(std::unique_ptr<abstract_task> t) noexcept : wrapped{std::move(t)} {}

    std::unique_ptr<abstract_task> wrapped;
};

// A vector of task, which wrap a unique pointer.
std::vector<task> tasks;

// We take a task by value, since it's constructible from a unique pointer.
void push(task t) {
    tasks.emplace_back(std::move(t));
}

但现在还是有些问题,some_task.wrapped->process()的用法会很难受,继续调整:

struct task {
    task(std::unique_ptr<abstract_task> t) noexcept : wrapped{std::move(t)} {}
    
    void process() {
        wrapped->process();
    }
    
private:
    std::unique_ptr<abstract_task> wrapped;
};

void run() {
    for(auto&& task : tasks) {
        task.process();
    }
    
    tasks.clear();
}

现在已经很不错了!对于任何地方 std::unique_ptr<abstract_task> ,你都可以放到 tasks里(隐式构造),且是 pass-by-value

push(std::make_unique<print_task>());

但是等等……这并没有解决我们的问题!我们想要支持 lambda,改变对象的发送方式,避免堆分配,这真的有用吗?

当然!在该列表中,我们现在可以做一件事:改变传递对象的方式。无需更改 200 个函数签名,我们只需更改 task 的构造函数。
现在,希望 push 函数能够接收 some_library_task 。为此,我们需要一个 Adaptor 来使这些类型适应接口abstract_task

// Our adapter. We contain a library task and implementing the abstract_task interface
struct some_library_task_adapter : abstract_task {
    some_library_task_adapter(some_library_task t) : task{std::move(t)} {}

    void process() override {
        task.process();
    }
    
    some_library_task task;
};

struct task {
    task(std::unique_ptr<abstract_task> t) noexcept : wrapped{std::move(t)} {}
    
    // We can now receive a library task by value.
    // We move it into a new instance of adapter.
    task(some_library_task t) noexcept :
        wrapped{std::make_unique<some_library_task_adapter>(std::move(t))} {}
    
    void process() {
        wrapped->process();
    }
    
private:
    std::unique_ptr<abstract_task> wrapped;
};

int main() {
    // push a new task to the vector
    push(some_library_task{});
}

到此,我们可以通过 pass-by-value 方式来 push 未继承 abstract_tasksome_library_task 对象。
但是,那些继承自 abstract_tasktask 还不能 pass-by-value,而必须使用 ptr。因此,我们需要将为每个类创建一个 Adaptor, 但我们不希望任何外部类扩展 abstract_task,因此它将是一个私有成员类型:

struct task {
    task(some_library_task task) noexcept :
        self{std::make_unique<library_model_t>(std::move(t))} {}
    task(print_task task) noexcept :
        self{std::make_unique<print_model_t>(std::move(t))} {}
    task(some_other_task task) noexcept :
        self{std::make_unique<some_other_model_t>(std::move(t))} {}
    
    void process() {
        self->process();
    }
    
private:
    // This is our interface, now named concept_t instead of abstract_task
    struct concept_t {
        virtual ~concept_t() = default;
        virtual void process() = 0;
    };
    
    // We name our struct `model` instead of `adapter`
    struct library_model_t : concept_t {
        library_model_t(some_library_task s) noexcept : self{std::move(s)} {}
        void process() override { self.process(); }
        some_library_task self;
    };
    
    struct print_model_t : concept_t {
        library_model_t(print_task s) noexcept : self{std::move(s)} {}
        void process() override { self.process(); }
        print_task self;
    };
    
    struct some_other_model_t : concept_t {
        library_model_t(some_other_task s) noexcept : self{std::move(s)} {}
        void process() override { self.process(); }
        some_other_task self;
    };
 
    // We quite know it's wrapped. Let's name it self
    std::unique_ptr<concept_t> self;
};

这太荒谬了!我们总不能为所有的 abstract_task 的派生类都复制一份构造函数,以及继承 concept_t的子类代码。

的确,C++ 中有一个很棒的工具,它经过精心设计,可以避免无意识的复制粘贴:模板!

struct task {
    template<typename T>
    task(T t) noexcept : self{std::make_unique<model_t<T>>(std::move(t))} {}
    
    void process() {
        self->process();
    }
    
private:
    struct concept_t {
        virtual ~concept_t() = default;
        virtual void process() = 0;
    };
    
    template<typename T>
    struct model_t : concept_t {
        model_t(T s) noexcept : self{std::move(s)} {}
        void process() override { self.process(); }
        T self;
    };

    std::unique_ptr<concept_t> self;
};

int main() {
    // natural syntax for object construction! Yay!
    push(some_library_task{});
    push(my_task{});
    push(print_task{});
}

问题解决了!我们代码的 API 中不再有复制粘贴,不再有继承,不再有指针!

三、Conclusion

这个 Concept-Model 是如何解决我们在开头列出的所有问题?
首先,它可以自然地应用多态性,与其他代码看起来很统一,语法也更简洁。

void do_stuff() {
    // Initialize a std::string using a value in direct initialization 
    std::string s{"value"};
    
    // Pretty similar syntax eh?
    task t{print_task{}};
    
    // Or if you like AAA style
    auto s2 = std::string{"potato"};
    auto t2 = task{print_task{}};
    
    // use string like this
    auto size = s.size();
    
    // use task like that. Again, pretty similar 
    t.process();
}

没有箭头,没有 new ,没有std::make_*。所有的多态性隐藏在实现细节中,潜在的生效的。其次,它避免了堆分配。是的,即使我们在内部通过唯一指针传递我们的对象。

void do_stuff() {
    some_task t;
    
    // do some stuff with task
    t.stuff();
    
    // maybe push the task
    if (condition()) {
        push(std::move(t));
    }
}

在上面示例中,t有条件地被推入列表。如果我们不需要堆分配和多态性,我们可以在运行时决定不使用它。还有其他策略,比如使用 SBO 来避免动态分配,我将在其他部分介绍。
第三,我们的任务实现可以按照 process 自己想要的方式实现功能。例如:

struct special_task {
    int process(bool more_stuff = false) const {
        // ...
    }
};

这仍然满足了这个概念。t.process() 即使函数是常量、接受可选参数或具有不同的返回类型,我们仍然可以调用。

标签:std,task,概念模型,process,self,Concept,Model,unique,void
From: https://www.cnblogs.com/CocoML/p/17481347.html

相关文章

  • 为什么RLHF中,PPO需要Critic模型而不是直接使用RewardModel
    在强化学习中,PPO(ProximalPolicyOptimization)算法是一种基于策略梯度的方法,用于训练强化学习智能体。PPO算法中引入Critic模型的主要目的是为了提供一个价值估计器,用于评估状态或状态动作对的价值,从而辅助策略的更新和优化。虽然奖励模型(RewardModel)可以提供每个状态或状态动作......
  • 识别一切模型RAM(Recognize Anything Model)及其前身 Tag2Text 论文解读
    总览大家好,我是卷了又没卷,薛定谔的卷的AI算法工程师「陈城南」~担任某大厂的算法工程师,带来最新的前沿AI知识和工具,欢迎大家交流~继MetaAI的SAM后,OPPO研究院发布识别一切模型(RecognizeAnythingModel,RAM):项目链接:https://recognize-anything.github.io/Demo链接:https://hugging......
  • 识别一切模型RAM(Recognize Anything Model)及其前身 Tag2Text 论文解读
    总览大家好,我是卷了又没卷,薛定谔的卷的AI算法工程师「陈城南」~担任某大厂的算法工程师,带来最新的前沿AI知识和工具,欢迎大家交流~继MetaAI的SAM后,OPPO研究院发布识别一切模型(RecognizeAnythingModel,RAM):项目链接:https://recognize-anything.github.io/Demo链接:https......
  • vue中v-model理解
    1.原理v-model是语法糖,相当于以下代码<inputv-model="value>/>等价于<input:value="value"v-on:input="this.value=$event.target.value"/>v-bind:value实现数据从data->组件input触摸事件实现数据从组件->datav-model在内部为不同的输入元素使用不同的属性并抛......
  • fastapi ResponseModel
    frompydanticimportBaseModelclassResponseModel(BaseModel):code:int=200msg:str=""success:bool=Truedata:dict=NoneclassTaskResponseModel(ResponseModel):data:dict={"code":200,......
  • 深入理解 DAO,DTO,DO,VO,AO,BO,POJO,PO,Entity,Model,View 各个模型对象的概念
    参考文档:https://blog.csdn.net/SR02020/article/details/105821816 深入理解DAO,DTO,DO,VO,AO,BO,POJO,PO,Entity,Model,View的概念DAO(DataAccessObject)数据访问对象DTO(DataTransferObject)数据传输对象DO(DomainObject)领域对象VO(ViewObject)视图模型AO(ApplicationObject)应用对象......
  • [转]POI 解析excel报错 java.lang.NoClassDefFoundError: org/apache/poi/ss/usermode
    前几天做了一个excel上传导入功能,为了通用想同步支持xls和xlsx格式。代码编写期并没有报错,所需要的类也都有。可是应用启动完测式功能的时候报了这么一个错Causedby:java.lang.NoClassDefFoundError:org/apache/poi/ss/usermodel/Date1904Support这是为什么呢?我第一感觉是jar......
  • listeners和v-model
    <template> <divid="app">  <LoadingButton@click="handlesClick"></LoadingButton>  <ceShi2></ceShi2> </div></template><script>importLoadingButtonfrom'@/compone......
  • 语言模型(Language Modeling)概述
    语言模型(LanguageModeling)是自然语言处理(NLP)领域的核心技术之一,它的目标是学习一种概率分布,用于表示自然语言文本中词汇和句子的组合。在本文中,我们将探讨语言模型的发展历史、技术细节以及应用方面。发展历史1.统计语言模型早期的语言模型主要基于统计方法,如N-gram模型。代表......
  • Navicat Premium将关系和实体添加到概念模型的方法
    NavicatPremium是一款强大的跨平台数据库管理工具,支持多种主流的关系型数据库系统,包括MySQL、MariaDB、SQLite、Oracle、PostgreSQL和MicrosoftSQLServer等。它提供了直观易用的用户界面和丰富的功能,使得数据库管理变得更加简单和高效。要添加关系,请单击工具栏中的新关系......