Подтвердить что ты не робот

Всегда объявляйте std:: mutex как изменяемый в С++ 11?

После просмотра сообщения Herb Sutter Вы не знаете, const и изменяемый, интересно, должен ли я всегда определять мьютекс как изменчивый? Если да, я думаю, что то же самое верно для любого синхронизированного контейнера (например, tbb::concurrent_queue)?

Некоторые предпосылки: в своем выступлении он заявил, что const == mutable == потокобезопасен, а std::mutex - для определения потокобезопасности.

Существует также связанный с этим вопрос о разговоре Содержит ли константа потокобезопасность в С++ 11.

Edit:

Здесь, я нашел связанный вопрос (возможно, дубликат). Однако он был задан до С++ 11. Возможно, это имеет значение.

4b9b3361

Ответ 1

Нет. Однако большую часть времени они будут.

Хотя полезно const как "потокобезопасное" и mutable как "(уже) поточно-безопасное", const по-прежнему в корне связано с понятием перспективного "я не изменюсь" это значение ". Это всегда будет.

У меня длинный ход мысли, так что несите меня.

В моем собственном программировании я помещал const всюду. Если у меня есть значение, то это плохо, если я скажу, что хочу. Если вы попытаетесь целенаправленно модифицировать const-объект, вы получите ошибку времени компиляции (легко исправить и не получить shippable result!). Если вы случайно измените объект, не являющийся объектом const, вы получите ошибку программирования во время выполнения, ошибку в скомпилированном приложении и головную боль. Так что лучше ошибиться на прежней стороне и сохранить вещи const.

Например:

bool is_even(const unsigned x)
{
    return (x % 2) == 0;
}

bool is_prime(const unsigned x)
{
    return /* left as an exercise for the reader */;
} 

template <typename Iterator>
void print_special_numbers(const Iterator first, const Iterator last)
{
    for (auto iter = first; iter != last; ++iter)
    {
        const auto& x = *iter;
        const bool isEven = is_even(x);
        const bool isPrime = is_prime(x);

        if (isEven && isPrime)
            std::cout << "Special number! " << x << std::endl;
    }
}

Почему типы параметров для is_even и is_prime отмечены const? Потому что с точки зрения реализации, изменение числа, которое я тестирую, будет ошибкой! Почему const auto& x? Потому что я не намерен изменять это значение, и я хочу, чтобы компилятор кричал на меня, если я это сделаю. То же самое с isEven и isPrime: результат этого теста не должен меняться, поэтому используйте его.

Конечно, функции-члены const - это просто способ дать this тип формы const T*. В нем говорится: "Было бы ошибкой в ​​реализации, если бы я изменил некоторые из моих членов".

mutable говорит "кроме меня". Здесь происходит "старое" понятие "логически const". Рассмотрим общий прецедент, который он дал: член мьютекса. Вам необходимо заблокировать этот мьютекс, чтобы убедиться, что ваша программа верна, поэтому вам нужно ее изменить. Вы не хотите, чтобы функция была не const const, потому что было бы ошибкой изменять любой другой член. Поэтому вы делаете это const и отмечаете мьютексы как mutable.

Ничто из этого не связано с безопасностью потоков.

Я думаю, что это слишком далеко, чтобы сказать, что новые определения заменяют старые идеи, приведенные выше; они просто дополняют его с другой точки зрения - безопасность потока.

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

struct foo
{
    void act() const
    {
        mNotThreadSafe = "oh crap! const meant I would be thread-safe!";
    }

    mutable std::string mNotThreadSafe;
};

Хорошо, поэтому мы знаем, что потокобезопасные вещи могут быть помечены как mutable, вы спрашиваете: должны ли они быть?

Я думаю, что мы должны рассматривать оба представления одновременно. Да, с Херба новая точка зрения. Они являются потокобезопасными, поэтому их не обязательно связывать с константой функции. Но только потому, что они могут быть освобождены от ограничений const, это не значит, что они должны быть. Мне все еще нужно подумать: было бы ошибкой в ​​реализации, если бы я модифицировал этого участника? Если это так, то это не должно быть mutable!

Здесь проблема детализации: некоторым функциям может потребоваться изменить потенциальный член mutable, а другие - нет. Это похоже на то, что только некоторые функции имеют доступ друг к другу, но мы можем только дружить со всем классом. (Это проблема языкового дизайна.)

В этом случае вы должны ошибаться со стороны mutable.

Трава говорила немного слабее, когда он дал пример const_cast, объявив его безопасным. Рассмотрим:

struct foo
{
    void act() const
    {
        const_cast<unsigned&>(counter)++;
    }

    unsigned counter;
};

Это безопасно в большинстве случаев, за исключением случаев, когда сам объект foo const:

foo x;
x.act(); // okay

const foo y;
y.act(); // UB!

Это описано в другом месте на SO, но const foo означает, что член counter также const, а модификация объекта const - это поведение undefined.

Вот почему вы должны ошибаться со стороны mutable: const_cast не дает вам одинаковых гарантий. Если бы counter был отмечен mutable, это не был бы объект const.

Хорошо, поэтому, если нам это нужно mutable в одном месте, мы нуждаемся в нем повсюду, и нам просто нужно быть осторожным в тех случаях, когда мы этого не делаем. Несомненно, это означает, что все нитевидные элементы должны быть отмечены mutable, а затем?

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

struct threadsafe_container_wrapper
{
    void missing_function_I_really_want()
    {
        container.do_this();
        container.do_that();
    }

    const_container_view other_missing_function_I_really_want() const
    {
        return container.const_view();
    }

    threadsafe_container container;
};

Здесь мы обертываем threadsafe_container и предоставляем другую функцию-член, которую хотим (на практике это будет лучше, чем свободная функция). Нет необходимости в mutable здесь, правильность со старой точки зрения полностью козыри: в одной функции я изменяю контейнер, и это хорошо, потому что я не сказал, что не буду (опуская const), а в другой я не изменяю контейнер и гарантирую, что соблюдаю это обещание (опуская mutable).

Я думаю, что Herb аргументирует большинство случаев, когда мы будем использовать mutable, мы также используем какой-то внутренний (потокобезопасный) объект синхронизации, и я согласен. Эрго его точка зрения работает большую часть времени. Но существуют случаи, когда у меня просто есть поточно-безопасный объект и просто рассматриваю его как еще один член; в этом случае мы отступаем от старого и фундаментального использования const.

Ответ 2

Я просто смотрел разговор, и я не совсем согласен с тем, что говорит Херб Саттер.

Если я правильно понял, его аргумент выглядит следующим образом:

  • [res.on.data.races]/3 накладывает требование на типы, которые используются со стандартной библиотекой - функции, не связанные с константой, должны быть потокобезопасными.

  • Поэтому const эквивалентен потокобезопасному.

  • И если const эквивалентен потокобезопасному, то mutable должен быть эквивалентен "доверять мне, даже не константные члены этой переменной являются потокобезопасными".

По-моему, все три части этого аргумента ошибочны (а вторая часть критически ошибочна).

Проблема с 1 заключается в том, что [res.on.data.races] предоставляет требования к типам в стандартной библиотеке, а не к типам, которые будут использоваться со стандартной библиотекой. Тем не менее, я думаю, что разумно (но не совсем ясно) интерпретировать [res.on.data.races], а также предоставлять требования к типам, которые будут использоваться со стандартной библиотекой, поскольку было бы практически невозможно, чтобы реализация библиотеки соблюдала требование не изменять объекты через const ссылки, если const функции-члены могли изменять объекты.

Критическая проблема с 2 заключается в том, что, хотя она истинна (если мы принимаем 1), что const должен подразумевать потокобезопасность, это не true, что потокобезопасность подразумевает const, и поэтому эти два не эквивалентны. const по-прежнему подразумевает "логически неизменяемый", просто расширяется область "логической неизменности", чтобы требовать безопасности потоков.

Если мы возьмем const и поточно-безопасный эквивалент, мы потеряем приятную функцию const, которая позволяет легко рассуждать о коде, видя, где значения могут быть изменены:

//`a` is `const` because `const` and thread-safe are equivalent.
//Does this function modify a?
void foo(std::atomic<int> const& a);

Кроме того, в соответствующем разделе [res.on.data.races] говорится о "модификациях", которые могут быть разумно интерпретированы в более общем смысле "изменений внешнего наблюдаемого пути", а не просто "изменениях в небезопасном виде",.

Проблема с 3 заключается просто в том, что она может быть истинна только в том случае, если 2 истинно, а 2 является критически ошибочным.


Итак, чтобы применить это к вашему вопросу - нет, вы не должны делать каждый внутренне синхронизированный объект mutable.

В С++ 11, как и в С++ 03, `const` означает "логически неизменяемый" и "изменяемый" означает "может измениться, но изменение не будет внешне наблюдаемым". Единственное отличие состоит в том, что в С++ 11 "логически неизменяемый" был расширен, чтобы включить "поточно-безопасный".

Вы должны зарезервировать mutable для переменных-членов, которые не влияют на внешнее видимое состояние объекта. С другой стороны (и это ключевой момент, который делает Herb Sutter в его разговоре), если у вас есть член, который по какой-то причине изменен, этот член должен быть внутренне синхронизирован, иначе вы рискуете сделать const безопасно, и это приведет к поведению undefined со стандартной библиотекой.

Ответ 3

Расскажите об изменении в const.

void somefunc(Foo&);
void somefunc(const Foo&);

В С++ 03 и ранее версия const по сравнению с не-t20 > обеспечивает дополнительные гарантии для вызывающих абонентов. Он promises не должен изменять свой аргумент, где под модификацией мы подразумеваем вызов Foo не-const-функций-членов (включая присваивание и т.д.) Или передачу его функциям, которые ожидают аргумент не const, или делают то же самое с его открытые не изменяемые данные. somefunc ограничивается const операциями на Foo. И дополнительная гарантия полностью односторонняя. Ни вызывающему абоненту, ни провайдеру Foo не нужно ничего делать, чтобы вызвать версию const. Любой, кто может вызвать версию не const, может также вызывать версию const.

В С++ 11 это изменяется. Версия const по-прежнему предоставляет ту же гарантию вызывающему, но теперь она поставляется с ценой. Поставщик Foo должен убедиться, что все операции const являются потокобезопасными. Или это должно быть минимальным, если somefunc является стандартной библиотечной функцией. Зачем? Поскольку стандартная библиотека может распараллелить свои операции, она будет вызывать операции const для чего угодно и без какой-либо дополнительной синхронизации. Таким образом, вы, пользователь, должны убедиться, что эта дополнительная синхронизация не требуется. Конечно, это не проблема в большинстве случаев, так как большинство классов не имеют изменяемых элементов, а большинство операций const не затрагивают глобальные данные.

Итак, что означает mutable? Это так же, как раньше! А именно, эти данные не являются константами, но это деталь реализации, я обещаю, что это не повлияет на наблюдаемое поведение. Это означает, что нет, вам не нужно отмечать все в поле зрения mutable, как вы не делали этого в С++ 98. Итак, когда вы должны пометить элемент данных mutable? Как и в С++ 98, когда вам нужно вызвать его операции const из метода const, и вы можете гарантировать, что он ничего не сломает. Повторить:

  • если физическое состояние элемента данных не влияет на наблюдаемое состояние объекта
  • и он потокобезопасен (внутренне синхронизирован)
  • тогда вы можете (если вам нужно!) продолжить и объявить его mutable.

Первое условие наложено, как и на С++ 98, потому что другой код, включая стандартную библиотеку, может вызывать ваши методы const, и никто не должен наблюдать за изменениями, вызванными такими вызовами. Второе условие есть, и это то, что нового в С++ 11, потому что такие вызовы могут выполняться асинхронно.

Ответ 4

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

Что Саттер ошибался в Const в С++ 11

TL: DR Const и Mutable оба подразумевают Thread-safe, но имеют разные значения в отношении того, что может и не может быть изменено в вашей программе.