Всегда объявляйте std:: mutex как изменяемый в С++ 11?
После просмотра сообщения Herb Sutter Вы не знаете, const и изменяемый, интересно, должен ли я всегда определять мьютекс как изменчивый? Если да, я думаю, что то же самое верно для любого синхронизированного контейнера (например, tbb::concurrent_queue
)?
Некоторые предпосылки: в своем выступлении он заявил, что const == mutable == потокобезопасен, а std::mutex
- для определения потокобезопасности.
Существует также связанный с этим вопрос о разговоре Содержит ли константа потокобезопасность в С++ 11.
Edit:
Здесь, я нашел связанный вопрос (возможно, дубликат). Однако он был задан до С++ 11. Возможно, это имеет значение.
Ответы
Ответ 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, но имеют разные значения в отношении того, что может и не может быть изменено в вашей программе.