Problem Statement:
Store properties with a certain name (key), type and value. The key is always a character literal and the value can be of type bool
, int
, float
, double
, char
, std::string
, Vector2f
, Vector2d
, Color3f
. It should be possible to add new properties and to retrieve the values of already existing properties. It is not expected that the value of a property is changed after adding it to the property set. The names of properties are unique and cannot be used multiple times (i.e. there can not be two properties with the same name). The number of properties is usually very low (less than 10 - it is not expected to have a high count here).
Implementation:
Here is a snippet of my proposed C++17 solution:
#include <map>
#include <sstream>
#include <string>
#include <variant>
...
class PropertySet {
public:
template<typename ValueType>
void addProperty(const std::string &name, const ValueType value) {
if (hasProperty(name)) {
// C++20 or https://github.com/fmtlib/fmt
// std::string msg = std::format("Property with name '{}' does
// already exist and its value is '{}'", name, value);
std::stringstream ss;
ss << "Property with name '"
<< name.c_str()
<< "' does already exist and its value is '"
<< value << "'";
throw PropertyDoesAlreadyExistException(ss.str());
} else {
values_[name] = value;
}
}
template<typename ValueType>
ValueType getProperty(const std::string &name) const {
if (hasProperty(name)) {
return std::get<ValueType>(values_.at(name));
} else {
std::stringstream ss;
ss << "Property with name '" << name.c_str() << "' does not exist";
throw PropertyDoesNotExistException(ss.str());
}
}
// A defaultValue can be provided in the case a property does not exist
template<typename ValueType>
ValueType getProperty(const std::string &name,
const ValueType defaultValue) const {
if (hasProperty(name)) {
return std::get<ValueType>(values_.at(name));
} else {
return defaultValue;
}
}
bool hasProperty(const std::string &name) const {
return values_.find(name) != values_.end();
}
private:
typedef std::variant<bool,
int,
float,
double,
char,
std::string,
Vector2f,
Vector2d,
Color3f> VariantType;
typedef std::map<std::string, VariantType> MapType;
MapType values_;
};
Test 1:
TEST(PropertySet, TestBasicTypes) {
PropertySet ps;
ps.addProperty("a", 1);
ps.addProperty("b", 1.0f);
ps.addProperty("c", 1.0);
ps.addProperty("d", 'a');
ps.addProperty("e", std::string("Hello World!"));
ps.addProperty("f", true);
EXPECT_EQ(ps.getProperty<int>("a"), 1);
EXPECT_EQ(ps.getProperty<int>("a"), 1);
EXPECT_EQ(ps.getProperty<float>("b"), 1.0f);
EXPECT_EQ(ps.getProperty<double>("c"), 1.0);
EXPECT_EQ(ps.getProperty<char>("d"), 'a');
EXPECT_EQ(ps.getProperty<std::string>("e"), "Hello World!");
EXPECT_EQ(ps.getProperty<bool>("f"), true);
}
Test 2:
TEST(PropertySet, WhenPropertyDoesNotExist_Then_ReturnDefaultValue) {
PropertySet ps;
EXPECT_THAT(ps.getProperty("notExistingProperty", 42), ::testing::Eq(42));
}
Test 3:
TEST(PropertySet, WhenSamePropertyIsAddedTwice_ThenThrowExceptionAndExpectProperErrorMessage) {
PropertySet ps;
ps.addProperty("a", 1);
EXPECT_THROW(ps.addProperty("a", 2), PropertyDoesAlreadyExistException);
try {
ps.getProperty<int>("a");
}
catch (PropertyDoesAlreadyExistException &ex) {
EXPECT_THAT(std::string(ex.what()),
::testing::Eq("Property with name 'a' does not exist and its value is '1'"));
}
}
I have more tests in place.
Questions:
Implementation
- Should
getProperty
return a (const) reference? - Should I switch for
name
toconst char*
instead of using astd::string
? - From a C++17 perspective: Are there more modern features of the language that I should use?
- Should
const T defaultValue
beconst T& defaultValue
? - Is it clear from the name what is the idea of the
defaultValue
? Should I add a comment here?
Testing
- Should Test 3 be split into two tests?
- Should I use
EXPECT_THAT
everywhere (EXPECT_THAT
vsEXPECT_EQ
)? - Are there any edge cases for which the implementation fails?
Do you have other feedback, improvements for the implementation and testing?