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