0

I'm using a single structure to store multiple parameters. I want to access these parameters via their element names, just like a normal structure. I also want to name the structure elements and access them using a string.

I came up with the following solution:

#include <string> #include <vector> #include <iostream> #include <stdexcept> #include <algorithm> struct Config { std::string paramThis; std::string paramOther; std::string paramMore; // man many more parameters. struct Element { const char *name; std::string *stringRef; }; const std::vector<Element> allElements = { {"this", &paramThis}, {"other", &paramOther}, {"more", &paramMore} }; const Element & findElem(const std::string &name) const { auto eIter = std::find_if( allElements.begin(), allElements.end(), [&name](const auto &elem) { return (name == elem.name); }); if (eIter == allElements.end()) { throw std::invalid_argument("unknown member element"); } return *eIter; } }; int main() { auto printByElem = [](const auto &elem) { std::cout << "by element: " << elem << " (" << (void *)&elem << ")\n"; }; auto printByName = [] (const auto &config, const auto &name) { auto stringRef = config.findElem(name).stringRef; std::cout << "by name: " << name << '=' << *stringRef << " (" << (void *)stringRef << ")\n"; }; Config testConf1 { .paramThis = "this data", .paramOther = "other data", .paramMore = "more data" }; std::cout << "ORIGINAL\n"; printByElem(testConf1.paramThis); printByElem(testConf1.paramOther); printByElem(testConf1.paramMore); printByName(testConf1, "this"); printByName(testConf1, "other"); printByName(testConf1, "more"); std::cout << '\n'; Config testConf2 = testConf1; std::cout << "COPY\n"; printByElem(testConf2.paramThis); printByElem(testConf2.paramOther); printByElem(testConf2.paramMore); printByName(testConf2, "this"); printByName(testConf2, "other"); printByName(testConf2, "more"); std::cout << '\n'; Config testConf3 = std::move(testConf1); std::cout << "MOVE\n"; printByElem(testConf3.paramThis); printByElem(testConf3.paramOther); printByElem(testConf3.paramMore); printByName(testConf3, "this"); printByName(testConf3, "other"); printByName(testConf3, "more"); std::cout << '\n'; return 0; } 

Works fine until i copy or move the struct:

ORIGINAL by element: this data (0x7ffe585ba700) by element: other data (0x7ffe585ba720) by element: more data (0x7ffe585ba740) by name: this=this data (0x7ffe585ba700) by name: other=other data (0x7ffe585ba720) by name: more=more data (0x7ffe585ba740) COPY by element: this data (0x7ffe585ba780) by element: other data (0x7ffe585ba7a0) by element: more data (0x7ffe585ba7c0) by name: this=this data (0x7ffe585ba700) by name: other=other data (0x7ffe585ba720) by name: more=more data (0x7ffe585ba740) MOVE by element: this data (0x7ffe585ba800) by element: other data (0x7ffe585ba820) by element: more data (0x7ffe585ba840) by name: this= (0x7ffe585ba700) by name: other= (0x7ffe585ba720) by name: more= (0x7ffe585ba740) 

The problem is the vector, with the pointers, which is also copied or moved.

I could of course write constuctors that copy or move the elements individually. But since I have a lot of parameters, I want to avoid that. It would also be very error-prone when making changes. A new parameter would have to be handled in too many places.

Another option would be to put the parameters in a separate substructure. However, this would not be as attractive in terms of use. For example, I would simply like to use the configuration as a parameter like this: someMethod({.paramThis = “Test”});. A substructure would interfere with this type of use.

Does anyone have an idea whether it is possible to prevent the copying or moving of a single element in a struct via the default constructors / default assignments? Or has another idea how to solve this elegantly?


Many thanks to anyone who tried to help. My solution is now this:

#include <string> #include <iostream> #include <stdexcept> #include <algorithm> struct Config { std::string paramThis; std::string paramOther; std::string paramMore; // man many more parameters. struct Element { const char *name; std::string Config::*stringRef; }; const Element & findElem(const std::string &name) const; }; static const Config::Element allElements[] = { {"this", &Config::paramThis}, {"other", &Config::paramOther}, {"more", &Config::paramMore} }; inline const Config::Element & Config::findElem(const std::string &name) const { auto eIter = std::find_if( std::begin(allElements), std::end(allElements), [&name](const auto &elem) { return (name == elem.name); }); if (eIter == std::end(allElements)) { throw std::invalid_argument("unknown member element"); } return *eIter; } int main() { auto printByElem = [](const auto &elem) { std::cout << "by element: " << elem << " (" << (void *)&elem << ")\n"; }; auto printByName = [] (const auto &config, const auto &name) { auto stringRef = config.findElem(name).stringRef; std::cout << "by name: " << name << '=' << config.*stringRef << " (" << (void *)&(config.*stringRef) << ")\n"; }; Config testConf1 { .paramThis = "this data", .paramOther = "other data", .paramMore = "more data" }; std::cout << "ORIGINAL\n"; printByElem(testConf1.paramThis); printByElem(testConf1.paramOther); printByElem(testConf1.paramMore); printByName(testConf1, "this"); printByName(testConf1, "other"); printByName(testConf1, "more"); std::cout << '\n'; Config testConf2 = testConf1; std::cout << "COPY\n"; printByElem(testConf2.paramThis); printByElem(testConf2.paramOther); printByElem(testConf2.paramMore); printByName(testConf2, "this"); printByName(testConf2, "other"); printByName(testConf2, "more"); std::cout << '\n'; Config testConf3 = std::move(testConf1); std::cout << "MOVE\n"; printByElem(testConf3.paramThis); printByElem(testConf3.paramOther); printByElem(testConf3.paramMore); printByName(testConf3, "this"); printByName(testConf3, "other"); printByName(testConf3, "more"); std::cout << '\n'; return 0; } 

I use an array instead of a map to create a compile-time structure.

13
  • why is name not a std::string ? Commented May 15 at 10:55
  • are all parameters strings? or are there other types? Its seems all you needs is a std::unordered_map<std::string,std::string> Commented May 15 at 10:59
  • @463035818_is_not_an_ai: It could. But a const char * does not need a constructor and does not allocate extra heap memory. Since i have to use c++14, i also could not use std::string_view. Commented May 15 at 11:02
  • 1
    You might turn allElements as function instead of variable member, then it won't suffer from copy. Caveat is that you would need to store it locally to be able to use begin()/end() on same container. Commented May 15 at 11:10
  • 1
    std::array, std::initializer_list doesn't own its data. Demo Commented May 15 at 11:25

4 Answers 4

6

You can avoid the issue of copying allElement by using member function pointers rather than bare pointers. The map does not need to be a member of the class at all though.

struct Config { std::string paramThis; std::string paramOther; std::string paramMore; // man many more parameters. }; const std::string& get_member(const std::string& name,const Config& config) { static const std::unordered_map<std::string, std::string Config::*> mapping { {"this",&Config::paramThis}, {"other",&Config::paramOther}, {"more",&Config::paramMore} }; auto it = mapping.find(name); if (it != mapping.end()) return (config.*(it->second)); throw "invalid name"; } 

This requires only minimal changes on your main to get expected output. Live Demo

Using std::unordered_map in place of your std::vector + std::find_if is not essential, it's just easier to write. Also turning the function into a free function is not essential, it can be a member. The important change is too look up the members not via pointers to specific object, but via member pointers. That map (mapping) could also be copied without loosing the ability to look up members of another instance.


If you want to have custom copy or move semantics you can supply your custom copy / move constructors. Though I advise to do so only when necessary. It is not necessary here.

If you still want it you would have to update the pointers in allElements to point to the right members on a copy or move. Not copying it would not be sufficient. Though repopulating the map (or vector) on every copy / move is rather expensive (compared to not doing anything extra).

Sign up to request clarification or add additional context in comments.

3 Comments

Good idea. But i don't want to construct a map and used an plain array to create a compile-time data struct instead.
@MarcusHampel you are not using a plain array, you are using a std::vector. As I wrote in the answer, that change is not essential to the answer. You can stay with your std::vector and the answer applies unchanged otherwise
@MarcusHampel you could even use an actual compile time array and it would still work as outlined in the answer. std::find_if can also find an element in a plain array
2

In the real-world scenario, I also have other types that I need to distinguish

If your struct Config contains different types, then on top of the suggested solution with pointers to members map, you might provide templated accessor specialized for the supported types:

struct Foo { int x; }; struct Config { std::string paramThis; double paramOther; Foo paramMore; template <typename T> T& findElem(std::string) { throw std::invalid_argument("element of type " + std::string(typeid(T).name()) + " not found"); } }; template <> std::string& Config::findElem<std::string>(std::string name) { if (name != "paramThis") { throw std::invalid_argument("element " + name + " not found"); } return paramThis; } template <> double& Config::findElem<double>(std::string name) { if (name != "paramOther") { throw std::invalid_argument("element " + name + " not found"); } return paramOther; } template <> Foo& Config::findElem<Foo>(std::string name) { if (name != "paramMore") { throw std::invalid_argument("element " + name + " not found"); } return paramMore; } TEST(xxx, yyy) { Config cfg{"someStr", 1.234, Foo{123}}; cfg.findElem<std::string>("paramThis") = "abc"; cfg.findElem<double>("paramOther") = 4.2; cfg.findElem<Foo>("paramMore").x = 42; std::cout << cfg.findElem<std::string>("paramThis") << '\n'; std::cout << cfg.findElem<double>("paramOther") << '\n'; std::cout << cfg.findElem<Foo>("paramMore").x << '\n'; ASSERT_ANY_THROW(cfg.findElem<double>("foo")); // "foo" not found ASSERT_ANY_THROW(cfg.findElem<int>("foo")); // type "int" not found } 

Each find_elem instead of if(name ... can implement a lookup using particular std::unordered_map<std::string, T Config::*>.

6 Comments

This is possible, but not easy to maintain. I came up with the array solution to have a single table with all the relationships from names to structure elements.
@MarcusHampel once the members have different types you need more than one table, or something more complicated. The approach in this answer needs one table per type of members. Its difficult to be better than that
@463035818_is_not_an_ai: I have only one more type: std::deque<std::string> which splits into to different serialization types ("array" and "list"). My element struct contains the name, a type (string/list or array) and two pointers (to std::string and std::deque<std::string>). That's it.
@MarcusHampel so you need two ways to map from string to member, and this answer shows how this can be done, no? Each map is still defined in one single place
@463035818_is_not_an_ai: I have an enumeration value (string/list/array) in the Element structure that specifies which pointer to use and how to handle the contents.
|
0

Unfortunately... not cleanly. C++ doesn't let you selectively exclude const members from default copy/move savior. You'd have to delete the copy/move constructors, or define them manually, which you're trying to avoid due to the sheer number of parameters. One alternatives option is compute-once, but non-owning, like:

struct Config { std::string paramThis, paramOther, paramMore; struct Element { const char *name; std::string *stringRef; }; std::vector<Element> getElements() { return { {"this", &paramThis}, {"other", &paramOther}, {"more", &paramMore} }; } Element findElem(const std::string& name) { // fresh and valid every time auto v = getElements(); ... } }; 

1 Comment

The solution has the problem that a new vector has to be created every time you want to access an element.
0

You should use std::(unordered_)map instead of std::vector. Let it handle the name lookups for you, that's literally what its designed for.

As for the pointer issue during copying/moving, you can use pointer-to-members instead, which don't change value during the program's lifetime. And then you can construct the map (or vector, or array, whatever you want to use) only 1 time and apply the pointer-to-members on multiple Config instances as needed.

For example:

#include <string> #include <iostream> #include <stdexcept> #include <unordered_map> #include <memory> struct Config { std::string paramThis; std::string paramOther; std::string paramMore; // many more parameters. const std::string & findElem(const std::string &name) const { static const std::unordered_map<std::string, std::string (Config::*)> allElements = { {"this", &Config::paramThis}, {"other", &Config::paramOther}, {"more", &Config::paramMore} }; auto eIter = allElements.find(name); if (eIter == allElements.end()) { throw std::invalid_argument("unknown member element"); } return this->*(eIter->second); } }; int main() { auto printByElem = [](const auto &elem) { std::cout << "by element: " << elem << " (" << std::addressof(elem) << ")\n"; }; auto printByName = [] (const auto &config, const auto &name) { auto &stringRef = config.findElem(name); std::cout << "by name: " << name << '=' << stringRef << " (" << std::addressof(stringRef) << ")\n"; }; Config testConf1 { .paramThis = "this data", .paramOther = "other data", .paramMore = "more data" }; std::cout << "ORIGINAL\n"; printByElem(testConf1.paramThis); printByElem(testConf1.paramOther); printByElem(testConf1.paramMore); printByName(testConf1, "this"); printByName(testConf1, "other"); printByName(testConf1, "more"); std::cout << '\n'; Config testConf2 = testConf1; std::cout << "COPY\n"; printByElem(testConf2.paramThis); printByElem(testConf2.paramOther); printByElem(testConf2.paramMore); printByName(testConf2, "this"); printByName(testConf2, "other"); printByName(testConf2, "more"); std::cout << '\n'; Config testConf3 = std::move(testConf1); std::cout << "MOVE\n"; printByElem(testConf3.paramThis); printByElem(testConf3.paramOther); printByElem(testConf3.paramMore); printByName(testConf3, "this"); printByName(testConf3, "other"); printByName(testConf3, "more"); std::cout << '\n'; return 0; } 

Demo

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.