@gunnerfunner, thanks again!
In the code I just posted, I meant
std::cout << get_name(g) << '\n';
And some other things which are now fixed.
--
Also, here is the rest of my previous post:
If the goal is to keep a collection of all sorts of blocks, this won't cut it, because every conceivable Block is a different type. You really do need runtime polymorphism, though, by definition: everything that does different things based on the kind of block you have is a sort of polymorphism.
The goal is to be able to write this:
1 2 3 4 5 6 7 8 9 10
|
class Block; // ...
struct DirtBlock { std::string name() const { return "dirt"; } };
struct GrassBlock { std::string name() const { return "grass"; } };
int main() {
GrassBlock g{};
DirtBlock d{};
Block dirt{d}, grass{g}; // same type! std::vector<Block> is allowed.
std::cout << dirt.name() << '\n' << grass.name() << '\n';
}
|
Our proposed class
Block
should be able to store anything that looks like a Block, without caring what it is. Note that DirtBlock and GrassBlock could both be passed to
get_name()
as defined above -- that is, they both share a common interface, but they are otherwise completely unrelated. Maybe surprisingly, this is possible without anything too arcane, and it suggests a technique called
type erasure.
You can use something like the following Block class to store every unrelated class that (non-intrusively) implements the Block interface, in a similar way that the first snippet indicates.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
namespace detail { // internal interface definition
struct BlockInterface {
// maybe provide assignment semantics here, or factored out
virtual ~BlockInterface() = default;
virtual std::string name() const = 0;
};
}
class Block: detail::BlockInterface {
template <typename T> struct RealBlock : detail::BlockInterface {
template <typename U> explicit RealBlock(U &&u) : t(std::forward<U>(u)) {}
std::string name() const { return t.name(); }
T t;
};
std::unique_ptr<BlockInterface> b;
public:
Block() = default;
// Subtle pitfall: constructor template must be explicit or otherwise not
// participate in overload resolution in the case that T deduces to a
// cv-qualified Block, otherwise it will conflict with move and copy
// constructors.
template <typename T>
explicit Block(T &&t) : b{std::make_unique<RealBlock<T>>(t)} {}
std::string name() const override { return b ? b->name() : "no name"; }
};
|
If you need to do things with one kind of block that you can't do with others, you have to know exactly what kind of block it is. This is the bottom line, although you can encode the dynamic type in some arbitrary way (or use RTTI) and get it back through downcasting -- that is, by using a switch at runtime.
In the most basic case this involves repeated
dynamic_cast
or the equivalent, but if needed, type erasure can make the process less exhaustive. You can also provide
default implementations of functions which certain Blocks do not implement at all, although this gets a little ugly.
Here's an example of how to do that, about as simple as I can make it right now. Feel free to ask if something's not clear.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
|
#include <iostream>
#include <memory>
#include <type_traits>
struct GrassBlock {
explicit GrassBlock(int x) : v{x} {}
std::string name() const { return "grass"; }
int value() const { return v; }
int v;
};
struct DirtBlock {
std::string name() const { return "dirt"; }
};
namespace detail {
// Dummy parameters cause the first overload to be preferred.
// expression SFINAE causes the second overload to be selected if
// substitution fails in the decltype expression
// This allows us to do something based on the return type,
// specifically, to select a default implementation when required
template <typename T>
static auto has_value_impl(int)
-> decltype(void(std::declval<T>().value()), std::true_type{});
template <typename> static std::false_type has_value_impl(long);
template <typename T> struct has_value : decltype(has_value_impl<T>(0)) {};
template <typename T> constexpr bool has_value_v = has_value<T>::value;
struct BlockInterface { // internal interface
// maybe provide assignment semantics here, or factored out
virtual std::string name() const = 0;
virtual int value() const = 0;
virtual ~BlockInterface() = default;
};
} // namespace detail
class Block : detail::BlockInterface {
template <typename T> struct RealBlock : detail::BlockInterface {
template <typename U> explicit RealBlock(U &&u) : t(std::forward<U>(u)) {}
std::string name() const override { return t.name(); }
template <bool UseDefault>
std::enable_if_t<!UseDefault, int> value_impl() const {
return t.value();
}
template <bool UseDefault>
std::enable_if_t<UseDefault, int> value_impl() const {
return 42;
}
int value() const override { return value_impl<!detail::has_value_v<T>>(); }
T t;
};
std::unique_ptr<BlockInterface> b;
public:
Block() = default;
// Subtle pitfall: constructor template must be explicit or otherwise not
// participate in overload resolution in the case that T deduces to a
// cv-qualified Block, otherwise it will conflict with move and copy
// constructors.
template <typename T>
explicit Block(T &&t) : b{std::make_unique<RealBlock<T>>(t)} {}
std::string name() const override { return b ? b->name() : "no name"; }
int value() const override { return b ? b->value() : 0; }
};
int main() {
GrassBlock g{2};
DirtBlock d{};
Block dirt{d}, grass{g};
std::cout << dirt.name() << '\n' << grass.name() << '\n';
std::cout << dirt.value() << '\n' << grass.value() << '\n';
}
|
Demo:
http://coliru.stacked-crooked.com/a/0031843707793850