Ключевое слово mutable на C++ часто неправильно понимается или упускается из виду, однако в ряде сценариев оно очень важно.
Подробно изучим их вместе с преимуществами mutable и потенциальными ошибками.
Что такое mutable на C++?
Это ключевое слово для изменения переменной-члена константного объекта. Посредством mutable указывается, что конкретный член данных константного объекта изменяется, не сказываясь на неизменяемости объекта.
Начнем с простого примера:
class Example {
public:
int getValue() const {
return value;
}
void setValue(int newValue) const {
value = newValue; // Ошибка: нельзя изменить член в константной функции
}
private:
int value;
};
В этом случае setValue не скомпилируется, потому что это попытка изменить value в константной функции. Если же объявить value как mutable:
class Example {
public:
int getValue() const {
return value;
}
void setValue(int newValue) const {
value = newValue; // Теперь получается
}
private:
mutable int value;
};
Теперь setValue скомпилируется и его работа обойдется без неожиданностей, хотя это константная функция.
Когда используется mutable
Ключевое слово mutable особенно кстати в сценариях:
- Кэширования.
- Отложенных вычислений.
- Синхронизации потоков.
- Сохранения логической неизменяемости.
Рассмотрим каждый сценарий с практическими примерами.
Кэширование с mutable
Кэширование — типичный сценарий для mutable. Возьмем класс, которым выполняется дорогостоящее вычисление:
class DataProcessor {
public:
DataProcessor(const std::vector<int>& data) : data_(data) {}
int getSum() const {
if (!cachedSum_) {
cachedSum_ = std::accumulate(data_.begin(), data_.end(), 0);
}
return *cachedSum_;
}
private:
std::vector<int> data_;
mutable std::optional<int> cachedSum_;
};
В этом примере cachedSum_ объявлена как mutable и, хотя это константная функция, изменяется в getSum(). При первом вызове getSum() вычисляется и кэшируется сумма. При дальнейших — возвращается кэшированное значение, так повышается производительность при сохранении неизменяемости объекта.
Отложенные вычисления
mutable используется для реализации отложенных вычислений, в этом случае значение вычисляется, только когда это необходимо:
class LazyString {
public:
LazyString(const std::string& str) : original_(str) {}
const std::string& getUpperCase() const {
if (!upperCase_) {
upperCase_ = original_;
std::transform(upperCase_->begin(), upperCase_->end(), upperCase_->begin(), ::toupper);
}
return *upperCase_;
}
private:
std::string original_;
mutable std::optional<std::string> upperCase_;
};
Здесь upperCase_ является mutable и вычисляется только при вызове getUpperCase(). Так экономятся память и вычислительное время, если версия в верхнем регистре останется невостребованной.
Синхронизация потоков
В многопоточном коде mutable используется с примитивами синхронизации:
#include <mutex>
class ThreadSafeCounter {
public:
int getValue() const {
std::lock_guard<std::mutex> lock(mutex_);
return value_;
}
void increment() {
std::lock_guard<std::mutex> lock(mutex_);
++value_;
}
private:
int value_ = 0;
mutable std::mutex mutex_;
};
В этом примере mutex_ объявлен как mutable. Поэтому блокируется и разблокируется в константных функциях-членах, чем обеспечивается потокобезопасность при сохранении неизменяемости.
Сохранение логической неизменяемости
Иногда объекту нужно изменить свое внутреннее состояние, не затрагивая его логического состояния. И здесь приходится кстати mutable:
class BigData {
public:
BigData(const std::vector<int>& data) : data_(data) {}
int get(size_t index) const {
if (index >= data_.size()) {
throw std::out_of_range("Index out of range");
}
++accessCount_;
return data_[index];
}
size_t getAccessCount() const {
return accessCount_;
}
private:
std::vector<int> data_;
mutable size_t accessCount_ = 0;
};
В этом примере accessCount_ является mutable, благодаря чему отслеживается, сколько раз осуществлялся доступ к данным без изменения логического состояния объекта. Эта информация полезна при отладке или оптимизации.
Потенциальные ошибки
mutable — полезный функционал, но применять его нужно с умом. Вот потенциальные ошибки и рекомендации:
- Потокобезопасность. Члены класса с mutable не являются потокобезопасными автоматически, в многопоточном коде необходимо обеспечить корректную синхронизацию:
class UnsafeCounter {
public:
void increment() const {
++count_; // Это не потокобезопасно
}
private:
mutable int count_ = 0;
};
2. Логическая неизменяемость. Когда изменяется член класса с mutable, не должно изменяться логическое состояние объекта:
class BadExample {
public:
void setData(int data) const {
data_ = data; // Этим нарушается логическая неизменяемость
}
private:
mutable int data_;
};
3. Ясность кода. mutable используется только при необходимости, злоупотребление им чревато запутанным и сложным в сопровождении кодом.
4. Документация. При использовании mutable рекомендуется документировать, почему он необходим:
class GoodExample {
public:
int getData() const {
return data_;
}
private:
// «mutable» используется здесь для кэширования с целью повышения производительности
mutable int data_ = 0;
};
Реальный сценарий: конфигурация с отложенной загрузкой
Рассмотрим пример с mutable посложнее — класс конфигурации, которым выполняется отложенная загрузка данных из файла:
#include <fstream>
#include <string>
#include <unordered_map>
#include <stdexcept>
class Configuration {
public:
Configuration(const std::string& filename) : filename_(filename) {}
std::string getValue(const std::string& key) const {
loadConfigIfNeeded();
auto it = config_.find(key);
if (it == config_.end()) {
throw std::runtime_error("Key not found: " + key);
}
return it->second;
}
private:
void loadConfigIfNeeded() const {
if (!isLoaded_) {
std::ifstream file(filename_);
if (!file) {
throw std::runtime_error("Unable to open config file: " + filename_);
}
std::string line;
while (std::getline(file, line)) {
size_t delimiterPos = line.find('=');
if (delimiterPos != std::string::npos) {
std::string key = line.substr(0, delimiterPos);
std::string value = line.substr(delimiterPos + 1);
config_[key] = value;
}
}
isLoaded_ = true;
}
}
std::string filename_;
mutable bool isLoaded_ = false;
mutable std::unordered_map<std::string, std::string> config_;
};
Здесь isLoaded_ и config_ являются mutable, поэтому из константной функции getValue() вызывается loadConfigIfNeeded(). Конфигурация загрузится, только когда впервые понадобится. Если же останется невостребованной, будут сэкономлены память и время запуска.
Проиллюстрируем примером:
int main() {
const Configuration config("settings.conf");
try {
std::cout << "Log level: " << config.getValue("log_level") << std::endl;
std::cout << "Max connections: " << config.getValue("max_connections") << std::endl;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
Таким образом объект Configuration получается константным, при этом отложенная загрузка его данных по-прежнему выполняется. Это потокобезопасно, притом что не осуществляется конкурентный доступ, эффективно и с сохранением логической неизменяемости.
Заключение
Ключевое слово mutable на C++ — это инструмент, при корректном применении которого пишется более эффективный и логически непротиворечивый код. Особенно кстати оно приходится при кэшировании, отложенной загрузке, синхронизации потоков и сохранении логической неизменяемости.
Однако важно использовать mutable с умом: злоупотребление чревато запутанным кодом и потенциальными проблемами потокобезопасности.
Читайте также:
- C++: практическое руководство по Transform
- C++: подробное руководство по вложенным операторам If-Else
- C++: подробное руководство по обработке файлов с getline()
Читайте нас в Telegram, VK и Дзен
Перевод статьи ryan: C++ Mutable: Comprehensive Guide





