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

2022-01-24 00:00:00 templates polymorphism c++ c++17 crtp

考虑以下代码,其中 Writer_I 充当接口.其他符合以正确形式编写元素类型的约定的类可以从它派生.这里,printf 和流被选为策略,Calculator 作为用户.

Consider the following code where the Writer_I acts as an interface. Other classes which fulfil the contract of writing element types in correct form can derive from it. Here, printf and streams are chosen as policies, and Calculator as user.

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

That interface is somehow stored in Calculator and write_i hides all the ugly details of templates so that class member functions remain clean. Most things remain known at compile time, and inline-able.

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

I know this is a classic case of virtual + derivation based polymorphism where a non-templated interface can be stored inside Calculator and write member function is called. But having known the type at compile time, and still deferring resolution to run time seems bad. It hints that some run time value will affect the chosen method of writing while that is not the case.

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

One way could be to make Calculator a template and keep its implementation in a cpp file and include the cpp file in tests. That's just nasty. Every method of Calculator will have a useless template <> on the top. And it's getting instantiated only once. (Twice if you could tests, but then if the only reason to make Calculator a template was tests, I'd say that tests are being too intrusive.)

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

I saw the talk https://www.youtube.com/watch?v=mU_n_ohIHQk (Meta Polymorphism - 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.我试过但很快就卡住了:如何拥有一个指向通用 lambda 的函数指针?

which showed a technique with std::any (which will store the Writer_I instance reference) + lambda (which contains the actual Impl type) + function pointer (which can be called later). Slides 79-83. I tried but got stuck real quick: How to have a function pointer to a generic lambda?

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

My solution, after all these futile attempts out of curiosity, would be to use iterator pattern and free the Calculator from the responsibility of "writing". Calculator should just be calculating data, not writing it. That solves the problem! Caller gets the data by running iterator++ and writes it any way it likes. Or may not even write it, but just test the numbers directly. Calculator remains a non template, thus in cpp files.

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

But if there's any way to achieve what I intend with the current design, I'd be happy to see it. I know there are some contradictory constraints, like using type erasure which may internally use virtual but curiosity is allowed on Stack Overflow, right (; ?

https://godbolt.org/z/W74833

澄清一下,这里的用户类是 Calculator,它不应该是一个模板.所有作者都可以保留在标题中,无需隐藏.对于 CRTP,实际上需要在 main 中了解每个 writer 实现的作用.

to clarify, here the user class is Calculator which should not be a template. All writers can remain in headers and need not be hidden. For CRTP, it is actually needed in main to know what each writer implementation does.

#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 << "
-------
" << a << std::endl;
  }
};

class Printf : public Writer_I<Printf>{
public:
  template <Elem elemtype, std::enable_if_t<elemtype == Elem::HEADER, int> = 0>
  void write(int a) {
    std::printf("%d
", a);
  }
  template <Elem elemtype, std::enable_if_t<elemtype == Elem::FOOTER, int> = 0>
  void write(float a) {
    std::printf("
--------
%f
", 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 不需要是模板,并且必须使用编写器进行初始化.

Your design constraint is that Calculator needs to not be a template, and has to be initialized with a writer.

这意味着它与编写器的接口必须是动态的.它可以通过虚拟接口类,通过存储函数指针,或稍后传递指针,或类似的方式.

That means its interface with the writer has to be dynamic. It can be through a virtual interface class, by storing function pointers, or by being passed pointers later, or similar.

由于您不想固定 writer 的多态接口,因此排除了虚拟接口.

As you don't want the polymorphic interface of writer to be fixed, that rules out a virtual interface.

现在,我们可以手动完成.

Now, we can do this manually.

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

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

这些是我们需要键入擦除的调用.我们需要输入erase down到他们的签名,然后记住如何做.

those are the calls we need to type erase. We need to type erase down to their signatures, and remember how to do it later.

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 作为第二个参数.

any_type_erase is a bit of a helper to do the boxing of the operation. For a const operation, pass in std::any const as the 2nd argument.

添加这些成员:

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 );
}

相关文章