Использование mutable для изменения объекта в unordered_set - PullRequest
0 голосов
/ 12 сентября 2018

Пожалуйста, рассмотрите следующий код :

#include <iostream>
#include <unordered_set>

struct MyStruct
{
    int x, y;
    double mutable z;

    MyStruct(int x, int y)
        : x{ x }, y{ y }, z{ 0.0 }
    {
    }
};

struct MyStructHash
{
    inline size_t operator()(MyStruct const &s) const
    {
        size_t ret = s.x;
        ret *= 2654435761U;
        return ret ^ s.y;
    }
};

struct MyStructEqual
{
    inline bool operator()(MyStruct const &s1, MyStruct const &s2) const
    {
        return s1.x == s2.x && s1.y == s2.y;
    }
};

int main()
{
    std::unordered_set<MyStruct, MyStructHash, MyStructEqual> set;
    auto pair = set.emplace(100, 200);

    if (pair.second)
        pair.first->z = 300.0;

    std::cout << set.begin()->z;
}

Я использую mutable, чтобы разрешить изменение члена z из MyStruct.Я хотел бы знать, если это нормально и законно, так как набор а) неупорядочен и б) я не использую z для хеширования или равенства?

Ответы [ 3 ]

0 голосов
/ 12 сентября 2018

Я бы сказал, что это идеальное использование ключевого слова "Mutable".

Ключевое слово mutable предназначено для пометки членов, которые не являются частью "состояния" класса (т.е. они представляют собой некоторую формукэшированного или промежуточного значения, которое не представляет логическое состояние объекта).

Ваш оператор равенства (а также другие компараторы (или любая функция, которая сериализует данные) (или функция, которая генерирует хэш))определить состояние объекта.Ваш компаратор равенства не использует член 'z', когда он проверяет логическое состояние объекта, поэтому член 'z' не является частью состояния класса и поэтому не может использовать «изменяемый» метод доступа.

Теперь говорю это.Я думаю, что код очень хрупкий, чтобы писать таким образом.В классе нет ничего, что мешало бы будущему сопровождающему случайно сделать z частью состояния класса (т.е. добавить его в хэш-функцию) и, таким образом, нарушить предварительные условия его использования в std::unordered_set<>.Так что вы должны быть очень рассудительны в использовании этого и тратить много времени на написание комментариев и модульных тестов, чтобы убедиться, что предварительные условия соблюдены.

Я бы также посмотрел комментарий "@Andriy Tylychko" о разбиении класса начасть const и часть значения, чтобы вы могли потенциально использовать ее как часть std::unordered_map.

0 голосов
/ 12 сентября 2018

Проблема в том, что z не является частью состояния объекта , только в контексте этого конкретного вида unordered_set. Если вы продолжите этот путь, то на всякий случай все будет изменяться.

В общем, то, что вы спрашиваете, невозможно, поскольку хеш элемента необходимо будет автоматически пересчитать при модификации элемента.

Самое общее, что вы можете сделать, это иметь протокол для модификации элемента, аналогичный функции modify в Boost.MultiIndex https://www.boost.org/doc/libs/1_68_0/libs/multi_index/doc/reference/ord_indices.html#modify. Код ужасен, но благодаря существованию extract его можно сделать достаточно эффективным, когда это имеет значение (ну, тем не менее, ваша конкретная структура не получит выгоды от перемещения).

template<class UnorderedSet, class It, class F>
void modify(UnorderedSet& s, It it, F f){
    It h = it; ++h;
    auto val = std::move(s.extract(it).value());
    f(val);
    s.emplace_hint(h, std::move(val) );
}

int main(){
    std::unordered_set<MyStruct, MyStructHash, MyStructEqual> set;
    auto pair = set.emplace(100, 200);

    if (pair.second) modify(set, pair.first, [](auto&& e){e.z = 300;});

    std::cout << set.begin()->z;
}

(код не проверен)


@ JoaquinMLopezMuños (автор Boost.MultiIndex) предложил переустановить весь узел. Я думаю, что это будет работать так:

template<class UnorderedSet, class It, class F>
void modify(UnorderedSet& s, It it, F f){
    It h = it; ++h;
    auto node = s.extract(it);
    f(node.value());
    s.insert(h, std::move(node));
}

EDIT2 : окончательный проверенный код, требуется C ++ 17 (для извлечения)

#include <iostream>
#include <unordered_set>

struct MyStruct
{
    int x, y;
    double  z;

    MyStruct(int x, int y)
        : x{ x }, y{ y }, z{ 0.0 }
    {
    }

};

struct MyStructHash
{
    inline size_t operator()(MyStruct const &s) const
    {
        size_t ret = s.x;
        ret *= 2654435761U;
        return ret ^ s.y;
    }
};

struct MyStructEqual
{
    inline bool operator()(MyStruct const &s1, MyStruct const &s2) const
    {
        return s1.x == s2.x && s1.y == s2.y;
    }
};

template<class UnorderedSet, class It, class F>
void modify(UnorderedSet& s, It it, F f){
    auto node = s.extract(it++);
    f(node.value());
    s.insert(it, std::move(node));
}

int main(){
    std::unordered_set<MyStruct, MyStructHash, MyStructEqual> set;
    auto pair = set.emplace(100, 200);

    if(pair.second) modify(set, pair.first, [](auto&& e){e.z = 300;});

    std::cout << set.begin()->z;
}
0 голосов
/ 12 сентября 2018

Типичным используемым изменяемым является то, что метод const позволяет изменять элемент данных, который не является частью основного состояния объекта, например лениво оцененное значение, полученное из неизменяемых данных объекта. Объявление открытых элементов данных непостоянным не является хорошей практикой, вы разрешаете внешнему изменению состояния объектов, даже если этот объект помечен как const.

В вашем примере кода вы использовали mutable, потому что (основываясь на вашем комментарии) ваш код не будет компилироваться без него. Ваш код не будет компилироваться, потому что итератор, возвращаемый из emplace, является константой.

Существует два неверных способа решения этой проблемы: один - использование ключевого слова mutable, другой, почти такой же плохой, - преобразование ссылки const в неконстантную ссылку.

Метод emplace предназначен для создания объекта непосредственно в коллекции и предотвращения вызова конструктора копирования. Это полезная оптимизация, но вы не должны использовать ее, если это поставит под угрозу поддержку вашего кода. Вам следует либо инициализировать z в своем конструкторе, либо не использовать emplace для добавления объекта в набор, вместо этого установите значение z и вставьте объект в ваш набор.

Если ваш объект никогда не должен изменяться после построения, вы должны сделать свой класс / структуру неизменяемым, объявив их const или объявив члены-данные приватными и добавив методы-мутанты доступа без изменений (они должны быть объявлены как const).

...