fatm
fatm

Reputation: 65

An unordered_map that returns pairs of different types c++

I am trying to implement an std::unordered_map that returns pairs of either double, int or std::string. The keys for the map are std::strings. Below is what I have tried so far:

#include <fstream>
#include <iostream>
#include <string>
#include <sstream>
#include <unordered_map>
#include <utility>
#include <vector>

// A base class for boundary class
class Boundbase {
    public:
    Boundbase(){};
    virtual ~Boundbase(){};
};

// A different map of boundaries for each different data type
template <class dType>
class Boundary : public Boundbase {
    std::pair<dType, dType> bpair;
    
    public:
    //Constructor
    Boundary(const std::string &lbound, 
        const std::string &ubound) {
        setbound(lbound, ubound);
    };

    //A method to set boundary pair
    void setbound(const std::string &lbound, 
        const std::string &ubound);
    
    // A method to get boundary pair
    std::pair<dType, dType> getbound() {return bpair;}
};

// Class to hold the different boundaries 
class Boundaries {
    std::unordered_map<std::string, Boundbase*> bounds;

    public:
    //Constructor
    Boundaries() {};

    // A method to set boundary map
    void setboundmap(std::unordered_map<std::string, 
            std::vector<std::string>> xtb);

    // A template to get boundaries.
    std::unordered_map<std::string, Boundbase*> getbounds()
        {return bounds;}
};

// A method to set covariate boundary
template <class dType> void
Boundary<dType>::setbound(const std::string &lbound, 
        const std::string &ubound) {
    dType val;
    std::istringstream isa(lbound);
    while(isa >> val) {
        bpair.first = val;
    }
    std::istringstream isb(ubound);
    while(isb >> val) {
        bpair.second = val;
    }
}

// A method to set boundary map
void Boundaries::setboundmap(std::unordered_map<std::string, 
        std::vector<std::string>> xtb) {
    for(auto s : xtb) {
        char type = s.second[1][0];
        switch(type) {
            case 'd': {
            std::pair<std::string, Boundbase*> opair;
            opair.first = s.first;
            opair.second = new Boundary<double>(
                    s.second[2], s.second[3]);
            bounds.insert(opair);
            }
            break;
            case 'i': {
            std::pair<std::string, Boundbase*> opair;
            opair.first = s.first;
            opair.second = new Boundary<int>(
                    s.second[2], s.second[3]);
            bounds.insert(opair);
            break;
            }
            case 'c': {
            std::pair<std::string, Boundbase*> opair;
            opair.first = s.first;
            opair.second = new Boundary<std::string>(
                    s.second[2], s.second[2]);
            bounds.insert(opair);
            break;
            }
        }
    }
}

This compiles ok using g++. When I try to run it though ( as follows):

int main() {
    Data D;
    Boundaries B;
    std::ifstream iss("tphinit.txt");
    D.read_lines(iss);

    auto dbounds = D.get_xtypebound();
    B.setboundmap(dbounds);

    auto tbounds = B.getbounds();
    auto sbound = tbounds["X1"];
    std::cout << sbound->bpair.first << "," 
        << sbound->bpair.second << std::endl;
}

I get 'class Boundbase' has no member named 'bpair' which is true because I am pointing to the base class and not the derived class. As far as I can tell, trying to get the derived member bpair requires that I use the visitor pattern. Now, it is clear that I am noob so when I had a look at different ways of doing this on SO I was a little in over my head (no reflection on the authors, just on my inexperience).

So my main question is: Is this the best and simplest way to go about this? I would like to avoid boost::variant if at all possible (mainly for the sake of purity: this cannot be that difficult). A sub-question is whether I have to use the visitor pattern or is there a better/simpler way to get the member pbair?

I will have to perform this lookup many times so I am hoping to make it as fast as possible but using the stl for the sake of simplicity.

Upvotes: 1

Views: 340

Answers (1)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275385

Make your values std variants over the 3 types.

Failing that, boost variant.

Std and boost variant really are what you want. You'll end up implementing some subset of its implementation.

Failing that, find a tutorial on how to implement ones of them, or use std any. Failing that, dynamic casts around an otherwise useless wrapper type with a virtual dtor stored in a unique ptr, or do manual RTTI with try get methods.

This just gets increasingly ugly and/or inefficient however.

Boost variant, and std variant from it, was implemented for a reason, and that reason was solving the exact problem you are describing in an efficient manner.

#include <tuple>
#include <utility>
#include <string>

template<class...Ts>
struct destroy_helper {
    std::tuple<Ts*...> data;
    destroy_helper( std::tuple<Ts*...> d ):data(d){}
    template<class T>
    static void destroy(T* t){ t->~T(); }
    template<std::size_t I>
    void operator()(std::integral_constant<std::size_t, I>)const {
        destroy( std::get<I>( data ) );
    }
};

struct construct_helper {
    template<class T, class...Args>
    void operator()(T* target, Args&&...args)const {
        ::new( (void*)target ) T(std::forward<Args>(args)...);
    }
};

template<std::size_t...Is>
struct indexes {};

template<std::size_t N, std::size_t...Is>
struct make_indexes:make_indexes<N-1, N-1, Is...> {};

template<std::size_t...Is>
struct make_indexes<0, Is...>{
    using type=indexes<Is...>;
};
template<std::size_t N>
using make_indexes_t = typename make_indexes<N>::type;

template<class F>
void magic_switch( std::size_t i, indexes<>, F&& f ) {}

template<std::size_t I0, std::size_t...Is, class F>
void magic_switch( std::size_t i, indexes<I0,Is...>, F&& f )
{
    if (i==I0) {
        f( std::integral_constant<std::size_t, I0>{} );
        return;
    }
    magic_switch( i, indexes<Is...>{}, std::forward<F>(f) );
}

template<class T0>
constexpr T0 max_of( T0 t0 ) {
    return t0;
}
template<class T0, class T1, class...Ts>
constexpr T0 max_of( T0 t0, T1 t1, Ts... ts ) {
    return (t1 > t0)?max_of(t1, ts...):max_of(t0, ts...);
}

template<class...Ts>
struct Variant{
  using Data=typename std::aligned_storage< max_of(sizeof(Ts)...), max_of(alignof(Ts)...)>::type;
  std::size_t m_index=-1;
  Data m_data;
  template<std::size_t I>
  using alternative_t=typename std::tuple_element<I, std::tuple<Ts...>>::type;
  using pointers=std::tuple<Ts*...>;
  using cpointers=std::tuple<Ts const*...>;

  template<class T> T& get(){ return *reinterpret_cast<T*>(&m_data); }
  template<class T> T const& get() const { return *reinterpret_cast<T*>(&m_data); }
  template<std::size_t I>
  alternative_t<I>& get(){ return std::get<I>(get_pointers()); }
  template<std::size_t I>
  alternative_t<I> const& get()const{ return std::get<I>(get_pointers()); }

  pointers get_pointers(){
    return pointers( (Ts*)&m_data... );
  }
  cpointers get_pointers()const{
    return cpointers( (Ts const*)&m_data... );
  }
  std::size_t alternative()const{return m_index;}

  void destroy() {
      if (m_index == -1)
        return;
      magic_switch(m_index, make_indexes_t<sizeof...(Ts)>{}, destroy_helper<Ts...>(get_pointers()));
  }

  template<std::size_t I, class...Args>
  void emplace(Args&&...args) {
      destroy();
      construct_helper{}( std::get<I>(get_pointers()), std::forward<Args>(args)... );
      m_index = I;
  }

  Variant()=default;

  Variant(Variant const&)=delete;//todo
  Variant&operator=(Variant const&)=delete;//todo
  Variant(Variant &&)=delete;//todo
  Variant&operator=(Variant &&)=delete;//todo

  ~Variant(){destroy();}
};

int main() {
    Variant<int, double, std::string> bob;
    bob.emplace<0>( 7 );
    bob.emplace<1>( 3.14 );
    bob.emplace<2>( "hello world" );
}

here is a really simple variant interface.

The hard part is turning a runtime index into which of the compile time indexes you want to use. I call that the magic switch problem.

You might also want to implement apply visitor.

...

Or...

template<class T>
struct Derived;
struct Base {
  virtual ~Base() {}
  template<class T>
  friend T* get(Base* base) {
    Derived<T>* self = dynamic_cast<T*>(base);
    return self?&self.t:nullptr;
  }
  template<class T>
  friend T const* get(Base const* base) {
    Derived<T> const* self = dynamic_cast<T const*>(base);
    return self?&self.t:nullptr;
  }
};
template<class T>
struct Derived:Base {
  Derived(T in):t(std::move(in)){}
  T t;
};

std::unordered_map<std::string, std::unique_ptr<Base>> map;

map["hello"] = std::unique_ptr<Base>( new Derived<int>(-1) );
map["world"] = std::unique_ptr<Base>( new Derived<double>(3.14) );

int* phello = get<int>(map["hello"]);
if (phello) std::cout << *hello << "\n";
double* pworld = get<double>(map["world"]);
if (pworld) std::cout << *world << "\n";

which is a seriously bargain-basement std::any.

Upvotes: 4

Related Questions