策略 class 设计但没有使整个用户 class 成为模板
Policy class design but without making the whole user class a template
考虑以下代码,其中 Writer_I
充当接口。其他 class 履行以正确形式编写元素类型的合同的 es 可以从它派生。这里,printf 和 streams 被选为策略,Calculator
被选为用户。
该接口以某种方式存储在 Calculator
中,并且 write_i
隐藏了模板的所有丑陋细节,以便 class 成员函数保持干净。大多数事情在编译时都是已知的,inline-able.
我知道这是一个 classic 虚拟 + 基于派生的多态性案例,其中 non-templated 接口可以存储在 Calculator
中,并且调用 write
成员函数.但是在编译时知道类型,仍然将解析推迟到 运行 时间似乎很糟糕。它暗示某些 运行 时间值会影响所选择的书写方法,但事实并非如此。
一种方法是制作 Calculator
模板并将其实现保留在 cpp 文件中,并将 cpp 文件包含在测试中。那太讨厌了。 Calculator
的每个方法顶部都会有一个无用的 template <>
。而且它只被实例化一次。 (如果你可以测试,那么两次,但是如果制作 Calculator
模板的唯一原因是测试,我会说测试过于侵入。)
我看到了谈话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。我试过了,但很快就卡住了:
我的解决方案,在所有这些出于好奇的徒劳尝试之后,将是使用 iterator pattern 并将 Calculator
从“写作”的责任中解放出来。 Calculator
应该只是计算数据,而不是写入数据。这解决了问题! Caller通过运行ningiterator++
获取数据,随意写入。或者甚至可以不写,直接测试数字。 Calculator
仍然是非模板,因此在 cpp 文件中。
但如果有任何方法可以实现我对当前设计的预期,我会很高兴看到它。
我知道有一些矛盾的约束,比如使用类型擦除,它可能在内部使用 virtual 但 Stack Overflow 允许好奇心,对吧 (; ?
编辑:澄清一下,这里的用户 class 是 Calculator
,不应该是模板。所有作者都可以保留在 headers 中,无需隐藏。对于 CRTP,main
实际上需要了解每个 writer 实现的功能。
#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, std::enable_if_t<elemtype == Elem::HEADER, int> = 0>
void write(int a) {
std::printf("%d\n", a);
}
template <Elem elemtype, std::enable_if_t<elemtype == Elem::FOOTER, 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;
}
模板可以有 non-template base ,所以你可以实际遵循这个方案如果它有益(也可以通过减少声明和完整类型的需要来节省编译时间):
// Public interface header
class Writer {
// virtual interfaces, common public interface;
};
这将是所有 Writer class 的基础 class。 header 将是简约的并且不包含模板代码
// 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
};
上面的代码将用于创建具体的 classes,并且仅在需要类型完整的地方使用。
// 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
?)来创建实例 class,例如 RealWriter
。 WriterInterface
这里充当 mixin,当然可能不止一个,但在那种情况下继承可能需要虚拟继承以避免次要 Writer
子对象。
您的设计约束是 Calculator
不能是模板,并且必须使用编写器进行初始化。
这意味着它与编写器的接口必须是动态的。它可以通过虚拟接口 class,通过存储函数指针,或稍后传递指针,或类似的方式。
由于您不想固定 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 );
}
考虑以下代码,其中 Writer_I
充当接口。其他 class 履行以正确形式编写元素类型的合同的 es 可以从它派生。这里,printf 和 streams 被选为策略,Calculator
被选为用户。
该接口以某种方式存储在 Calculator
中,并且 write_i
隐藏了模板的所有丑陋细节,以便 class 成员函数保持干净。大多数事情在编译时都是已知的,inline-able.
我知道这是一个 classic 虚拟 + 基于派生的多态性案例,其中 non-templated 接口可以存储在 Calculator
中,并且调用 write
成员函数.但是在编译时知道类型,仍然将解析推迟到 运行 时间似乎很糟糕。它暗示某些 运行 时间值会影响所选择的书写方法,但事实并非如此。
一种方法是制作 Calculator
模板并将其实现保留在 cpp 文件中,并将 cpp 文件包含在测试中。那太讨厌了。 Calculator
的每个方法顶部都会有一个无用的 template <>
。而且它只被实例化一次。 (如果你可以测试,那么两次,但是如果制作 Calculator
模板的唯一原因是测试,我会说测试过于侵入。)
我看到了谈话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。我试过了,但很快就卡住了:
我的解决方案,在所有这些出于好奇的徒劳尝试之后,将是使用 iterator pattern 并将 Calculator
从“写作”的责任中解放出来。 Calculator
应该只是计算数据,而不是写入数据。这解决了问题! Caller通过运行ningiterator++
获取数据,随意写入。或者甚至可以不写,直接测试数字。 Calculator
仍然是非模板,因此在 cpp 文件中。
但如果有任何方法可以实现我对当前设计的预期,我会很高兴看到它。 我知道有一些矛盾的约束,比如使用类型擦除,它可能在内部使用 virtual 但 Stack Overflow 允许好奇心,对吧 (; ?
编辑:澄清一下,这里的用户 class 是 Calculator
,不应该是模板。所有作者都可以保留在 headers 中,无需隐藏。对于 CRTP,main
实际上需要了解每个 writer 实现的功能。
#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, std::enable_if_t<elemtype == Elem::HEADER, int> = 0>
void write(int a) {
std::printf("%d\n", a);
}
template <Elem elemtype, std::enable_if_t<elemtype == Elem::FOOTER, 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;
}
模板可以有 non-template base ,所以你可以实际遵循这个方案如果它有益(也可以通过减少声明和完整类型的需要来节省编译时间):
// Public interface header
class Writer {
// virtual interfaces, common public interface;
};
这将是所有 Writer class 的基础 class。 header 将是简约的并且不包含模板代码
// 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
};
上面的代码将用于创建具体的 classes,并且仅在需要类型完整的地方使用。
// 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
?)来创建实例 class,例如 RealWriter
。 WriterInterface
这里充当 mixin,当然可能不止一个,但在那种情况下继承可能需要虚拟继承以避免次要 Writer
子对象。
您的设计约束是 Calculator
不能是模板,并且必须使用编写器进行初始化。
这意味着它与编写器的接口必须是动态的。它可以通过虚拟接口 class,通过存储函数指针,或稍后传递指针,或类似的方式。
由于您不想固定 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 );
}