Walter
Walter

Reputation: 45414

which container to use map or set or else?

If using std::map<std::string,object> to keep objects ordered by name, the map's value_type (std::pair<string,object>) is a convenient class which warrants useful functionality, e.g.

typedef std::map<std::string,object> object_map;
typedef typename object_map::value_type named_object;
void function(named_object const&);

// with the following possible use case
void foo(std::string const&name, object_map const &omap)
{
  auto obj=omap.find(name);
  if(obj==omap.end()) throw std::runtime_error("name not found");
  function(*obj);
}

So far, so good. I later extend my objects

struct extended_object : object { /* more data */ };

and keep the extended objects ordered by name, too, via std::map<std::string,extended>. I may define.

typedef std::map<std::string,extended_object> extended_object_map;
typedef typename extended_object_map::value_type named_extended_object;

Unfortunately, I cannot

void foo(std::string const&name, extended_object_map const& emap)
{
  auto eobj=emap.find(name);
  if(eobj==emap.end()) throw std::runtime_error("name not found");
  function(*eobj);        // cannot convert *eobj to const named_object&
}

Now, my question is how to solve this problem? I considered using a reinterpret_cast<>

  function(reinterpret_cast<const named_object&>(*eobj));

which essentially assumes that the data layouts of named_object and named_extended_object are exactly like that of base and derived. Is this safe/recommendable? Alternatively, I considered to use std::set (instead of std::map) with key types named_object and re-define

struct named_extended_object : named_object { /* more data */ };

The problem with this approach is that in order to std::set::find() an object, I must provide not just a name string, but a whole object or even extended object. According to cppreference, std::set::find() will have a resolution to this problem in C++14 (overloads 3&4), but what shall I do in the mean time?

Upvotes: 0

Views: 125

Answers (2)

Jonathan Wakely
Jonathan Wakely

Reputation: 171263

the map's value_type (std::pair<string,object>) is a convenient class which warrants useful functionality

N.B. that is not the map's value_type. It's std::pair<const string,object>

The reinterpret_cast results in undefined behaviour when a named_extended_object is accessed through the reference, it violates what some compilers call "strict aliasing" rules, and can cause your program to misbehave if the compiler optimises using type-based alias analysis. Do not use reinterpret_cast like that. In handwavy Liskov Substitution Principle terms, extended_object IS-A object, so it's OK to substitute an extended_object where an object is expected, but pair<T, extended_object> IS-NOT-A pair<T, object> so cannot be used interchangeably.

I like D Drmmr's answer, but if you don't want to do that, another option is a template:

template<typename Obj>
  void function(const std::pair<const std::string, Obj>&)
  {
    // ...
  }

This will accept pair<const string, object> and also pair<const string, extended_object> arguments. If the body of the template only tries to access the members that are present in object then it will work perfectly for both argument types.

If you don't want to expose that template to users of the function, then declare two overloads:

void function(const named_object&);
void function(const named_extended_object&);

Then in the implementation file:

namespace {
  template<typename Obj>
    void function_impl(const std::pair<const std::string, Obj>&)
    {
      // common implementation ...
    }
}

void function(const named_object& no) { function_impl(no); }
void function(const named_extended_object& neo) { function_impl(neo); }

Upvotes: 1

D Drmmr
D Drmmr

Reputation: 1243

Change your function to

void function(const std::string& name, const object& obj);

Then call it like

auto obj = omap.find(name);
function(obj->first, obj->second);
// or
auto eobj = emap.find(name);
function(eobj->first, eobj->second);

Upvotes: 1

Related Questions