问题
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.
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.
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.
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.)
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
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?
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.
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
EDIT: 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 << "\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;
}
回答1:
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.
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);
}
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
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.
Add these members:
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 );
}
回答2:
Templates can have non-template base , so you may actually follow this scheme if its beneficial (can also save compile time by reducing need in declarations and complete types):
// Public interface header
class Writer {
// virtual interfaces, common public interface;
};
That would be base class for all Writer classes. The header would be minimalistic and doesn't include template code
// 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
};
Code above would be used to create concrete classes and only where it's required to have types complete.
// The real writer
template <>
class WriterTypeTraits<RealWriter> {
// definitions for RealWriter
};
class RealWriter : WriterInterface <RealWriter> {
// implementation details, initialization for WriterInterface
// members specific to RealWriter
};
And we can have some kind of factory or creator functions (make_real_writer
?) that creates of instances classes like RealWriter
. WriterInterface
here acts as mixin and of course there might be more than one of them, but in that case inheritance may require virtual inheritance to avoid secondary Writer
subobjects.
回答3:
here are most of the errors gone except for line 41 #include #include
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>>
void write(int a) {
std::cout << a << std::endl;
}
template <Elem elemtype, std::enable_if_t<elemtype == Elem::FOOTER>>
void write(float a) {
std::cout << "\n-------\n" << a << std::endl;
}
};
/* Restrictions being that member functions header and footer
remain in cpp files. And creators of Calculator
can specify alternative implementations. */
class Calculator {
std::_Any_tag writer;
public:
template <typename Impl>
Calculator(Writer_I<Impl>& writer) : writer(writer) {}
template <Elem elemtype, typename... T> void write_i(T &&...args) {
/* <MAGIC_CAST to Impl as above> */ writer<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(streams);
calc.header();
return 0;
}
来源:https://stackoverflow.com/questions/65628159/policy-class-design-but-without-making-the-whole-user-class-a-template