我应该更喜欢mixin还是函数模板来将行为添加到一组不相关的类型中

Should I prefer mixins or function templates to add behavior to a set of unrelated types?

本文关键字:一组 不相关 类型 添加 mixin 更喜欢 函数模板 我应该      更新时间:2023-10-16

Mixin和函数模板是向一组广泛的类型提供行为的两种不同方式,只要这些类型满足某些要求。

例如,假设我想编写一些代码,允许我将对象保存到文件中,只要该对象提供toString成员函数(这是一个相当愚蠢的示例,但请耐心等待)。第一个解决方案是编写如下函数模板:

template <typename T>
void toFile(T const & obj, std::string const & filename)
{
    std::ofstream file(filename);
    file << obj.toString() << 'n';
}
...
SomeClass o1;
toFile(o1, "foo.txt");
SomeOtherType o2;
toFile(o2, "bar.txt");

另一种解决方案是使用混合蛋白,使用CRTP:

template <typename Derived>
struct ToFile
{
    void toFile(std::string const & filename) const
    {
        Derived * that = static_cast<Derived const *>(this);
        std::ofstream file(filename);
        file << that->toString() << 'n';
    }
};
struct SomeClass : public ToFile<SomeClass>
{
    void toString() const {...}
};
...
SomeClass o1;
o.toFile("foo.txt");
SomeOtherType o2;
o2.toFile("bar.txt");

这两种方法的优缺点是什么?是否有受青睐的人,如果有,为什么?

第一种方法要灵活得多,因为它可以与任何提供任何方式转换为std::string的类型一起工作(这可以使用traits类实现),而无需修改该类型。您的第二种方法总是需要修改类型才能添加功能。

Pro函数模板:耦合比较松散。您不需要从任何东西派生就可以获得新类中的功能;在您的示例中,您只实现了toString方法,仅此而已。由于没有指定toString的类型,您甚至可以使用有限形式的duck类型。

Pro mixins:严格来说,什么都没有;您的需求是与不相关的类一起工作的东西,而mixin会使它们变得相关。

编辑:好吧,由于C++类型系统的工作方式,mixin解决方案将严格生成不相关的类。不过,我还是选择模板函数解决方案。

我想提出一个替代方案,但经常被遗忘,因为它是鸭子类型和接口的混合体,很少有语言提出这一壮举(注意:实际上非常接近Go对接口的理解)。

// 1. Ask for a free function to exist:
void toString(std::string& buffer, SomeClass const& sc);
// 2. Create an interface that exposes this function
class ToString {
public:
  virtual ~ToString() {}
  virtual void toString(std::string& buffer) const = 0;
}; // class ToString
// 3. Create an adapter class (bit of magic)
template <typename T>
class ToStringT final: public ToString {
public:
  ToStringT(T const& t): t(t) {}
  virtual void toString(std::string& buffer) const override {
    toString(buffer, t);
  }
private:
  T t;                  // note: for reference you need a reference wrapper
                        // I won't delve into this right now, suffice to say
                        // it's feasible and only require one template overload
                        // of toString.
}; // class ToStringT
// 4. Create an adapter maker
template <typename T>
ToStringT<T> toString(T const& t) { return std::move(ToStringT<T>(t)); }

现在呢?享受

void print(ToString const& ts); // aka: the most important const
int main() {
  SomeClass sc;
  print(toString(sc));
};

这两个阶段有点重量级,但它提供了惊人的功能:

  • 没有硬接线数据/接口(多亏了鸭子打字)
  • 低耦合(得益于抽象类)

而且易于集成

  • 您可以为已经存在的接口编写一个"适配器",并从OO代码库迁移到更灵活的代码库
  • 您可以为一组已经存在的重载编写一个"接口",并从泛型代码库迁移到更集群的代码库

除了锅炉板的数量之外,你如何从两个世界中无缝地选择优势真的很神奇。

我在写这个问题时的一些想法:

支持模板函数的参数:

  • 函数可以重载,因此可以处理第三方类型和内置类型

支持mixin的论点:

  • 同质语法:添加的行为与任何其他成员函数一样被调用。然而,众所周知,C++类的接口不仅包括其公共成员函数,还包括在这种类型的实例上操作的自由函数,因此这只是美学上的改进
  • 通过向mixin添加一个非模板基类,我们获得了一个接口(在Java/C#意义上),可以用来处理提供行为的所有对象。例如,如果我们使ToFile<T>继承自FileWritable(声明一个纯虚拟toFile成员函数),我们就可以拥有FileWritable的集合,而不必求助于复杂的异构数据结构

关于用法,我认为函数模板在C++中更为惯用。

相关文章: