通过数据重对象的元组索引无序映射

时间:2018-05-14 08:30:31

标签: c++ c++11

我有std::unordered_map<std::tuple<A, A>, B> map;。我有一个修改这样的地图的功能

void modify(const A& a1, const A& a2)
{
    map[/* a1, a2 */].modify();
}

现在我有点担心A的不必要的副本。这是我的尝试。

map[{a1, a2}].modify();

它看起来很干净,但它从a1a2的副本构建临时密钥(元组)。

map[std::tie(a1, a2)].modify();

这看起来很有希望,因为它构建了std::tuple<const A&, const A&>并将其传递给地图的operator[]。我的地图的operator[]签名是

B& operator[](const std::tuple<A, A>&)
B& operator[](std::tuple<A, A>&&)

哪个与std::tie的返回类型不匹配,但它有效。所以我看一下std::tuple的构造函数,并发现转换构造函数,这让我想到,副本仍然是(so I tested it)。

有没有办法查询地图,没有任何不必要的副本,仍然保留O(1)平均查找复杂度?

2 个答案:

答案 0 :(得分:5)

我最好的猜测是你不能避免在这里复制。整个问题归结为这样的事情:

A x1;
std::tuple<A&> t1{x1};
const std::tuple<A>& t2{t1};
const std::tuple<A>& t3{std::tuple<A&>{x1}};

t2t3的两种结构都会调用A的复制构造函数(实时演示:https://wandbox.org/permlink/MxTUb61kO3zL3HmD)。

如果您真的关心性能并且A的实例复制起来很昂贵,您可以将它们放入某个池(例如std::vector<A>),然后只将指针放入地图中({ {1}})。

<强>更新

请注意,您还可以设计自己的“元组/对”类,可以通过值或引用来构造。一个非常基本的解决方案可能如下:

std::unordered_map<std::tuple<A*,A*>,B>

在map中的元素访问期间,它的用法不会触发任何复制/移动构造函数:

struct construct_from_ref_tag { };

template <typename T> class ref_pair {
  public:
    ref_pair(T v1, T v2)
      : v1_(std::move(v1)), v2_(std::move(v2)), r1_(v1_), r2_(v2_) { }
    ref_pair(const T& r1, const T& r2, construct_from_ref_tag)
      : r1_(r1), r2_(r2) { }
    bool operator==(const ref_pair<T>& rhs) const {
      return ((r1_ == rhs.r1_) && (r2_ == rhs.r2_)); }
    size_t hash() const {
      return std::hash<T>{}(r1_) ^ std::hash<T>{}(r2_); }
  private:
    T v1_, v2_;
    const T& r1_;
    const T& r2_; 
};

namespace std {
  template <typename T> struct hash<ref_pair<T>> {
    size_t operator()(const ref_pair<T>& v) const { return v.hash(); }
  };
}

我不喜欢它,但它有效。 std::unordered_map<ref_pair<A>, int> map; map[ref_pair<A>(1, 2)] = 3; A a1{1}; A a2{2}; std::cout << "before access" << std::endl; map[ref_pair<A>(a1, a2, construct_from_ref_tag{})] += 1; 必须是默认构造的,默认构造函数应该是便宜的。现场演示:https://wandbox.org/permlink/obSfPEJXn3Yr5oRw

答案 1 :(得分:0)

我认为你不能避免复制。我没有使用元组,而是编写了一个基本的模板对类,并使用移动语义将其替换为您网站上的代码,这就是我提出的:

template<typename T>
struct Pair {
    T t1;
    T t2;

    Pair<T>() : t1(), t2() {}
    Pair<T>( T a, T b ) : t1( std::move(a) ), t2( std::move(b) ) {}

    Pair<T>( const Pair<T>& p ) : t1( std::move( p.t1 ) ), t2( std::move( p.t2 ) ) {}
    Pair<T>( Pair<T>&& p ) : t1( std::move( p.t1 ) ), t2( std::move( p.t2 ) ) {}

    Pair<T>& operator=( const Pair<T>& p ) {
        t1 = std::move( p.t1 );
        t2 = std::move( p.t2 );
        return *this;
    }

    Pair<T>& operator=( Pair<T>&& p ) {
        t1 = std::move( p.t1 );
        t2 = std::move( p.t2 );
        return *this;
    }
};

struct verbose {
    explicit verbose( int val ) : val( val ) { std::cout << " default ctor\n"; }
    ~verbose() { std::cout << val << " dtor\n"; }

    verbose( const verbose& v ) : val( v.val ) { std::cout << val << " copy ctor\n"; }
    verbose( verbose&& v ) : val( v.val ) { std::cout << val << " move ctor\n"; }

    verbose& operator=( const verbose& v ) {
        val = v.val;
        std::cout << val << " copy assign\n";
        return *this;
    }
    verbose& operator=( verbose&& v ) {
        val = v.val;
        std::cout << val << " move assign " << std::endl;
        return *this;
    }

    int val;
};

struct verbose_hash {
    std::size_t operator()( const Pair<verbose>& v ) const {
        return std::hash<int>{}(std::move( v.t1 ).val ^ std::move( v.t2 ).val);
    }
};

struct verbose_eq {
    bool operator()( const Pair<verbose>& lhs, const Pair<verbose>& rhs ) const {
        return (std::move( lhs ).t1).val == (std::move( rhs ).t1).val &&
            (std::move( lhs ).t2).val == (std::move( rhs ).t2).val;
    }
};

std::unordered_map<Pair<verbose>, int, verbose_hash, verbose_eq> map;

void modify1( const verbose& v1, const verbose& v2 ) {
    map[{v1, v2}] = 1;
}

void modify2( const verbose& v1, const verbose& v2 ) {
    //map[std::tie(v1, v2)] = 1;  // won't work not using tuple
}

int main() { 

    map.emplace(  std::move( Pair<verbose>( verbose(1) , verbose(2) ) ), std::move( 0 ) );
    std::cout << "After emplace\n";

    verbose v1( 1 ), v2( 2 );
    std::cout << "Before lookup1\n";
    modify1( std::move(v1), std::move(v2) );
    std::cout << "Before dtors\n";

    std::cout << "\nPress any key and enter to quit.\n";
    std::cin.get();
    return 0;
}

输出:

 default ctor
1 move ctor
2 move ctor
1 dtor
2 dtor
1 move ctor
2 move ctor
2 dtor
1 dtor
After emplace
 default ctor
 default ctor
Before lookup1
2 copy ctor        // copy
1 copy ctor        // copy
1 move ctor
2 move ctor
1 dtor
2 dtor
2 dtor
1 dtor
Before dtors

代码使用移动语义,但因为函数通过const引用绑定到左值;你不会远离副本。