微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

策略类设计,但没有使整个用户类成为模板

如何解决策略类设计,但没有使整个用户类成为模板

考虑以下代码,其中 Writer_I 充当接口。其他履行以正确形式编写元素类型的契约的类可以从它派生。此处,printf 和流被选为策略,Calculator 被选为用户

该接口以某种方式存储在 Calculator 中,write_i 隐藏了模板的所有丑陋细节,以便类成员函数保持干净。大多数东西在编译时仍然是已知的,并且可以内联。

我知道这是基于虚拟 + 派生的多态性的经典案例,其中可以将非模板化接口存储在 Calculator 中并调用 write 成员函数。但是在编译时知道类型,并且仍然将解析推迟到运行时似乎很糟糕。它暗示某些运行时值会影响所选的写入方法,而事实并非如此。

一种方法是使 Calculator 成为模板并将其实现保存在 cpp 文件中,并在测试中包含 cpp 文件。那太恶心了。 Calculator 的每个方法都会在顶部有一个无用的 template <>。而且它只会被实例化一次。 (如果你可以测试两次,但如果让 Calculator 成为模板的唯一原因是测试,我会说测试太具有侵入性。)

我看到了演讲 https://www.youtube.com/watch?v=mU_n_ohIHQk(元多态性 - Jonathan Boccara - Meeting C++ 2020 opening Keynote)https://meetingcpp.com/mcpp/slides/2020/meta_polymorphism_pdf3243.pdf

展示了一种技术:std::any(将存储 Writer_I 实例引用)+ lambda(包含实际的 Impl 类型)+ 函数指针(稍后可以调用)。幻灯片 79-83。我试过了,但很快就卡住了:How to have a function pointer to a generic lambda?

在出于好奇做了所有这些徒劳的尝试之后,我的解决方案是使用 iterator pattern 并将 Calculator 从“写作”的责任中解放出来。 Calculator 应该只是计算数据,而不是写入数据。这样问题就解决了!调用者通过运行 iterator++ 获取数据并以任何它喜欢的方式写入。或者甚至可能不会写它,而只是直接测试数字。 Calculator 仍然是非模板,因此在 cpp 文件中。

但是,如果有任何方法可以通过当前的设计实现我的意图,我会很高兴看到它。 我知道有一些相互矛盾的约束,比如使用类型擦除,它可能在内部使用 virtual 但在 Stack Overflow 上允许好奇,对 (; ?

https://godbolt.org/z/W74833

编辑:澄清一下,这里的用户类是 Calculator,它不应该是模板。所有作者都可以保留在标题中,不需要隐藏。对于 CRTP,main 实际上需要知道每个编写器实现的作用。

#include <any>
#include <iostream>
#include <type_traits>
#include <utility> 

enum class Elem {
  HEADER,FOOTER,};

template <typename Impl> class Writer_I {
public:
  template <Elem elemtype,typename... T> decltype(auto) write(T &&...args) {
    return static_cast<Impl *>(this)->template write<elemtype>(
        std::forward<T>(args)...);
  }
  virtual ~Writer_I() {}
};

class Streams : public Writer_I<Streams> {
public:
  template <Elem elemtype,std::enable_if_t<elemtype == Elem::HEADER,int> = 0>
  void write(int a) {
    std::cout << a << std::endl;
  }
  template <Elem elemtype,std::enable_if_t<elemtype == Elem::FOOTER,int> = 0>
  void write(float a) {
    std::cout << "\n-------\n" << a << std::endl;
  }
};

class Printf : public Writer_I<Printf>{
public:
  template <Elem elemtype,int> = 0>
  void write(int a) {
    std::printf("%d\n",a);
  }
  template <Elem elemtype,int> = 0>
  void write(float a) {
    std::printf("\n--------\n%f\n",a);
  } 
};

/* Restrictions being that member functions header and footer
   remain in cpp files. And callers of Calculator's constructor
   can specify alternative implementations. */
class Calculator {
  std::any writer;

public:
  template <typename Impl>
  Calculator(Writer_I<Impl> &writer) : writer(writer) {}

  template <Elem elemtype,typename... T> void write_i(T &&...args) {
    // MAGIC_CAST ----------------------↓
    auto a = std::any_cast<Writer_I<Printf>>(writer);
    a.write<elemtype>(std::forward<T>(args)...);
  }

  void header() {
    for (int i = 0; i < 10; i++) {
      write_i<Elem::HEADER>(i);
    }
  }

  void footer() {
    write_i<Elem::FOOTER>(-100.0f);
  }
};
int main() {
  Streams streams;
//   Calculator calc_s(streams); // throws bad_cast.
//   calc_s.header();
//   calc_s.footer();


  Printf printf_;
  Calculator calc_p(printf_); 
  calc_p.header();
  calc_p.footer();
  return 0;
}

解决方法

您的设计约束是 Calculator 不需要是模板,并且必须使用编写器进行初始化。

这意味着它与作者的接口必须是动态的。它可以通过一个虚拟接口类,通过存储函数指针,或者通过稍后传递指针,或类似的。

由于您不想修复 writer 的多态接口,因此排除了虚拟接口。

现在,我们可以手动执行此操作。

void header() {
  for (int i = 0; i < 10; i++) {
    write_i<Elem::HEADER>(i);
  }
}

void footer() {
  write_i<Elem::FOOTER>(-100.0f);
}

这些是我们需要键入擦除的调用。我们需要输入擦除他们的签名,并记住以后怎么做。

template<class T>
struct tag_t { using type=T; };
template<class T>
constexpr tag_t<T> tag = {};

template<class Sig,class Any=std::any>
struct any_type_erase;
template<class R,class...Args,class Any>
struct any_type_erase<R(Args...)> {
  std::function<R(Any&,Args&&...args)> operation;

  any_type_erase() = default;
  any_type_erase(any_type_erase const&) = default;
  any_type_erase(any_type_erase &&) = default;
  any_type_erase& operator=(any_type_erase const&) = default;
  any_type_erase& operator=(any_type_erase &&) = default;

  template<class T,class F>
  any_type_erase(tag_t<T>,F&& f) {
    operation = [f=std::forward<F>(f)](Any& object,Args&&...args)->R {
      return f(*std::any_cast<T*>(&object),std::forward<Args>(args)...);
    };
  }

  R operator()(Any& any,Args...args)const {
    return operation(any,std::forward<Args>(args)...);
  }
};

any_type_erase 有点帮助做操作的装箱。对于 const 操作,将 std::any const 作为第二个参数传入。

添加这些成员:

std::any writer;
any_type_erase<void(int)> print_header;
any_type_erase<void(float)> print_footer;

template<class T>
static auto invoke_writer() {
  return [](auto& writer,auto&&..args) {
    writer.write<T>(decltype(args)(args)...);
  };
}

template<typename Impl>
Calculator(Writer_I<Impl>& writer) :
  writer(writer),print_header( tag<Writer_I<Impl>>,invoke_writer<Elem::HEADER>() ),print_footer( tag<Writer_I<Impl>>,invoke_writer<Elem::FOOTER>() )
{}

void header() {
  for (int i = 0; i < 10; i++) {
    print_header( writer,i );
  }
}

void footer() {
  print_footer( writer,-100.0f );
}
,

模板可以有非模板 base ,所以如果它有益的话,你实际上可以遵循这个方案(也可以通过减少声明和完整类型的需要来节省编译时间):

// Public interface header

class Writer {
   // virtual interfaces,common public interface;
};

那将是所有 Writer 类的基类。标题将是简约的,不包含模板代码

// Private interface header (or unit if it is used in only one unit)
// As it contains actual implementation,it may require more headers
// or modules included than public header.

template < class Type > class WriterTypeTraits;

template < class Type >
class WriterInterface : class Writer,class WriterTypeTraits<Type> {
  // internal implementation of virtuals,some may use CRTP 
  // construction,initialization and deletion depending on Type
  // private members that shouldn't be seen
};

上面的代码将用于创建具体的类,并且仅在需要类型完整的地方使用。

// The real writer
template <>
class WriterTypeTraits<RealWriter> {
  // definitions for RealWriter
};

class RealWriter : WriterInterface <RealWriter> {
  // implementation details,initialization for WriterInterface 
  // members specific to RealWriter
};

而且我们可以有某种工厂或创建者函数(make_real_writer?)来创建像 RealWriter 这样的实例类。 WriterInterface 在这里充当 mixin,当然可能有多个,但在这种情况下,继承可能需要虚拟继承以避免次要 Writer 子对象。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。