Note: This is an updated, rewritten answer. The prior version is available in the edit history.
Let's look at it from data modeling perspective: Table
has a property height
. That property can either be owned by the Table
, or can come from another source. The simplest way one could think of would be to use a pointer to a const object. The pointer should be to a const, since the height may be a preset, and those shouldn't be changeable via the Table
object.
class Table {
int m_myHeight;
const int *m_height = &m_myHeight;
public:
int height() const { return *m_height; }
void setHeight(int newHeight) {
m_height = &m_myHeight;
m_myHeight = newHeight;
}
void setPresetHeight(const int &preset)
{
m_height = &preset;
/* this line is optional */ m_myHeight = *m_height;
}
};
class PresetHeights {
std::vector<int> m_data;
public:
const int &getPreset(int index);
};
This will work just fine, but you may wish to have some additional properties assigned to the preset - properties that the "embedded" height of the Table object doesn't have. For example, the preset can have a name, etc.
This can be done by holding a reference to either "just" a height, or to a height preset. Since the identifier of a height preset is used to index the identifier in a presets table, it probably makes sense to make the id value private, and only accessible via the presets table. This gives the power over ids to the presets table, and gives some freedom in how the table is implemented.
The example that follows is written in C++17, and can be tried out on godbolt.
#include <string>
#include <variant>
struct Height {
int value;
Height(int value) : value(value) {}
operator int() const { return value; }
};
struct HeightPreset : Height {
using Id = int;
private:
Id id; // the unique identifier of this preset
friend class HeightPresets;
friend int main(); // test harness
public:
std::string name; // name of this preset
template <typename Name>
HeightPreset(Id id, int value, Name &&name) :
Height(value), id(id), name(std::forward<Name>(name)) {}
};
The Table
uses std::variant
to hold either no height value (std::monostate
), or a Height
, or a HeightPreset
:
#include <functional>
class Table {
using preset_t = std::reference_wrapper<const HeightPreset>;
std::variant<std::monostate, Height, preset_t> m_height;
public:
std::optional<Height> height() const {
if (auto *customHeight = std::get_if<Height>(&m_height))
return *customHeight;
else if (auto *presetHeight = std::get_if<preset_t>(&m_height))
return std::get<preset_t>(m_height).get();
else
return {};
}
void setHeight(Height newHeight)
{ m_height = newHeight; }
void setHeightPreset(const HeightPreset &preset)
{ m_height = std::cref(preset); }
bool hasPresetHeight() const { return m_height.index() == 2; }
const HeightPreset &presetHeight() const
{ return std::get<preset_t>(m_height).get(); }
};
Presets Iterable By Value of Type HeightPreset
The presets are a map from HeightPreset::Id
to HeightPreset
. But first, we need an iterator adapter to let us iterate the preset values - hiding the implementation detail that we use a map whose iterated values are std::pair
, not HeightPreset
.
#include <map>
template <class K, class V, class C, class A>
class map_cvalue_iterator
{
typename std::map<K, V, C, A>::const_iterator it;
public:
map_cvalue_iterator(typename std::map<K,V>::const_iterator it) : it(it) {}
map_cvalue_iterator(const map_cvalue_iterator &o) : it(o.it) {}
auto &operator=(const map_cvalue_iterator &o) { it = o.it; return *this; }
auto operator++(int) { auto val = *this; ++it; return val; }
auto &operator++() { ++it; return *this; }
auto operator--(int) { auto val = *this; --it; return val; }
auto &operator--() { --it; return *this; }
const V& operator*() const { return it->second; }
const V* operator->() const { return it->second; }
bool operator==(map_cvalue_iterator o) const { return it == o.it; }
bool operator!=(map_cvalue_iterator o) const { return it != o.it; }
};
template <class M>
using map_cvalue_iterator_type
= map_cvalue_iterator<typename M::key_type, typename M::mapped_type,
typename M::key_compare, typename M::allocator_type>;
The presets are a thin wrapper around std::map
:
class HeightPresets {
public:
using Id = HeightPreset::Id;
HeightPresets(std::initializer_list<HeightPreset> presets)
{
for (auto &preset : presets)
m_presets.insert({preset.id, preset});
}
auto &get(Id id) const { return m_presets.at(id); }
Id getIdFor(const HeightPreset &preset) const
{ return preset.id; }
auto begin() const { return map_cvalue_iterator_type<map_t>(m_presets.cbegin()); }
auto end() const { return map_cvalue_iterator_type<map_t>(m_presets.cend()); }
private:
using map_t = std::map<Id, HeightPreset>;
map_t m_presets;
};
The simple test harness that demonstrates the use of those types:
#include <cassert>
int main() {
const HeightPresets presets{
{1, 5, "A Fiver"},
{2, 10, "A Tenner"}
};
Table aTable;
assert(!aTable.height());
// The table has no height by default
aTable.setHeight(10);
assert(!aTable.hasPresetHeight());
// The height was not a preset
assert(aTable.height() == 10);
// The height was retained
for (auto &preset : presets)
{
aTable.setHeightPreset(preset);
assert(aTable.hasPresetHeight());
// The height was preset
assert(aTable.height() == preset);
// The height has the expected preset's value
assert(presets.getIdFor(aTable.presetHeight()) == preset.id);
// The height has the expected preset's identifier
assert(aTable.presetHeight().name == preset.name);
}
}
Presets Iterable by std::pair<HeightPreset::Id, HeightPreset>
If you wish not to use the iterator adapter, it can be removed. See below, and also try it out on godbolt.
#include <map>
class HeightPresets {
public:
using Id = HeightPreset::Id;
HeightPresets(std::initializer_list<HeightPreset> presets)
{
for (auto &preset : presets)
m_presets.insert({preset.id, preset});
}
auto &get(Id id) const { return m_presets.at(id); }
Id getIdFor(const HeightPreset &preset) const
{ return preset.id; }
auto begin() const { return m_presets.cbegin(); }
auto end() const { return m_presets.cend(); }
private:
using map_t = std::map<Id, HeightPreset>;
map_t m_presets;
};
And the test harness:
#include <cassert>
int main() {
const HeightPresets presets{
{1, 5, "A Fiver"},
{2, 10, "A Tenner"}
};
Table aTable;
assert(!aTable.height());
// The table has no height by default
aTable.setHeight(10);
assert(!aTable.hasPresetHeight());
// The height was not a preset
assert(aTable.height() == 10);
// The height was retained
for (auto &presetPair : presets)
{
auto &preset = presetPair.second;
aTable.setHeightPreset(preset);
assert(aTable.hasPresetHeight());
// The height was preset
assert(aTable.height() == preset);
// The height has the expected preset's value
assert(presets.getIdFor(aTable.presetHeight()) == preset.id);
// The height has the expected preset's identifier
assert(aTable.presetHeight().name == preset.name);
}
}