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 (; ?
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;
}