I used to define my template requirement through abstract class, e.g.
#include <iostream>
#include <random>
/// Generic interface
template<typename A, typename B>
struct Interface {
virtual A callback_A(const std::vector<A>& va) = 0;
virtual const B& callback_B() = 0;
};
/// Mixin style, used to "compose" using inheritance at one level, no virtual
struct PRNG_mt64 {
std::mt19937_64 prng;
explicit PRNG_mt64(size_t seed) : prng(seed) {};
};
/// Our implementation
template<typename A>
struct Implem :
public Interface<A, std::string>,
public PRNG_mt64 {
std::string my_string{"world"};
explicit Implem(size_t seed) : PRNG_mt64(seed) {}
A callback_A(const std::vector<A>& a) override { return a.front(); }
const std::string& callback_B() override { return my_string; }
};
/// Function using our type. Verification of the interface is perform "inside" the function
template<typename T>
void use_type(T& t) {
auto& strings = static_cast<Interface<std::string, std::string>&>(t);
std::cout << strings.callback_A({"hello"}) << " " << strings.callback_B() << std::endl;
auto& prng = static_cast<PRNG_mt64&>(t).prng;
std::uniform_real_distribution<double> dis(0.0, 1.0);
std::cout << dis(prng) << std::endl;
}
int main(int argc, char **argv) {
size_t seed = std::random_device()();
Implem<std::string> my_impl(seed);
use_type(my_impl);
}
One benefit of using the asbtract class is the clear specification of the interface, easily readable. Also, Implem
has to confom to it (we cannot forget the pure virtual).
A problem is that the interface requirement is hidden in the static cast (that comes from my real use case where a composite "state" is used by several polymorphic components - each component can cast the state to only see what it needs to see). This is "solved" by concepts (see below).
Another one is that we are using the virtual mechanism when we have no dynamic polymorphism at all, so I would like to get rid of them. What is the best way to convert this "interface" into concept? I came up with this:
#include <iostream>
#include <random>
/// Concept "Interface" instead of abstract class
template<typename I, typename A, typename B>
concept Interface = requires(I& impl){
requires requires(const std::vector<A>& va){{ impl.callback_A(va) }->std::same_as<A>; };
{ impl.callback_B() } -> std::same_as<const B&>;
};
/// Mixin style, used to "compose" using inheritance at one level, no virtual
struct PRNG_mt64 {
std::mt19937_64 prng;
explicit PRNG_mt64(size_t seed) : prng(seed) {};
};
/// Our implementation
template<typename A>
struct Implem : public PRNG_mt64 {
std::string my_string{"world"};
/// HERE: requires in the constructor to "force" interface. Can we do better?
explicit Implem(size_t seed) requires(Interface<Implem<A>, A, std::string>): PRNG_mt64(seed) {}
A callback_A(const std::vector<A>& a) { return a.front(); }
const std::string& callback_B() { return my_string; }
};
/// Function using our type. Verification of the interface is now "public"
template<Interface<std::string, std::string> T>
void use_type(T& t) {
std::cout << t.callback_A({"hello"}) << " " << t.callback_B() << std::endl;
auto& prng = static_cast<PRNG_mt64&>(t).prng;
std::uniform_real_distribution<double> dis(0.0, 1.0);
std::cout << dis(prng) << std::endl;
}
int main(int argc, char **argv) {
size_t seed = std::random_device()();
Implem<std::string> my_impl(seed);
use_type(my_impl);
}
Questions:
Is that actually the thing to do in the first place? I saw several posts on the internet explaning concepts, but they are always so shallow that I'm afraid I'll miss something regarding perfect forwarding, move, etc...
I used a
requires requires
clause to keep function arguments close to their usage (useful when having many methods). However, the "interface" information is now hard to read: can we do better?Also, the fact that
Implem
implements the interface is now the part that is "hidden" inside the class. Can we make that more "public" without having to write another class with CRTP, or limiting the boilerplate code as much as possible?Can we do better for the "mixin" part
PRNG_mt64
? Ideally, turning this into a concept?
Thank you!
CodePudding user response:
Your pre-C 20 approach is pretty bad, but at least it sounds like you understand the problems with it. Namely, you're paying 8 bytes for a vptr when you don't need it; and then strings.callback_B()
is paying the cost of a virtual call even though you could be calling t.callback_B()
directly.
Finally (this is relevant, I promise), by funneling everything through the base-class reference strings
, you're taking away Implem
's ability to craft a helpful overload set. I'll show you a simpler example:
struct Interface {
virtual int lengthOf(const std::string&) = 0;
};
struct Impl : Interface {
int lengthOf(const std::string& s) override { return s.size(); }
int lengthOf(const char *p) { return strlen(p); }
};
template<class T>
void example(T& t) {
Interface& interface = t;
static_assert(!std::same_as<decltype(interface), decltype(t)>); // Interface& versus Impl&
int x = interface.lengthOf("hello world"); // wastes time constructing a std::string
int y = t.lengthOf("hello world"); // does not construct a std::string
}
int main() { Impl impl; example(impl); }
The generic-programming approach would look like this, in C 20:
template<class T>
concept Interface = requires (T& t, const std::string& s) {
{ t.lengthOf(s) } -> convertible_to<int>;
};
struct Impl {
int lengthOf(const std::string& s) override { return s.size(); }
int lengthOf(const char *p) { return strlen(p); }
};
static_assert(Interface<Impl>); // sanity check
template<Interface T>
void example(T& t) {
Interface auto& interface = t;
static_assert(std::same_as<decltype(interface), decltype(t)>); // now both variables are Impl&
int x = interface.lengthOf("hello world"); // does not construct a std::string
int y = t.lengthOf("hello world"); // does not construct a std::string
}
int main() { Impl impl; example(impl); }
Notice that there is no way at all to get back the "funneling" effect you had with the base-class approach. Now there is no base class, the interface
variable itself is still statically a reference to an Impl
, and calling lengthOf
will always consider the full overload set provided by the Impl
. This is a good thing for performance — I think it's a good thing in general — but it is radically different from your old approach, so, be careful!
For your callback_A/B
example specifically, your concept would look like
template<class T, class A, class B>
concept Interface = requires (T& impl, const std::vector<A>& va) {
{ impl.callback_A(va) } -> std::same_as<A>;
{ impl.callback_B() } -> std::same_as<const B&>;
};
In real life I would very strongly recommend changing those same_as
es to convertible_to
s instead. But this code is already very contrived, so let's not worry about that.
In C 17 and earlier, the equivalent "concept" (type-trait) definition would look like this (complete working example in Godbolt). Here I've used a macro DV
to shorten the boilerplate; I wouldn't actually do that in real life.
#define DV(Type) std::declval<Type>()
template<class T, class A, class B, class>
struct is_Interface : std::false_type {};
template<class T, class A, class B>
struct is_Interface<T, A, B, std::enable_if_t<
std::is_same_v<int, decltype( DV(T&).callback_A(DV(const std::vector<A>&)) )> &&
std::is_same_v<int, decltype( DV(T&).callback_B() )>
>> : std::true_type {};