Запрет создания временных объектов
Вопрос
При отладке сбоя в многопоточном приложении я наконец обнаружил проблему в следующем операторе:
CSingleLock(&m_criticalSection, TRUE);
Обратите внимание, что создается безымянный объект класса CSingleLock, и, следовательно, объект критической секции разблокируется сразу после этого оператора.Это явно не то, чего хотел программист.Эта ошибка была вызвана простой опечаткой.Мой вопрос в том, могу ли я каким-то образом предотвратить создание временного объекта класса во время самой компиляции, т.е.приведенный выше тип кода должен генерировать ошибку компилятора.В общем, я считаю, что всякий раз, когда класс пытается получить какой-либо ресурс, временный объект этого класса не должен быть разрешен.Есть ли способ обеспечить его соблюдение?
Решение
Редактировать: Как отмечает j_random_hacker, можно заставить пользователя объявить именованный объект, чтобы снять блокировку.
Однако даже если для вашего класса каким-то образом запрещено создание временных объектов, то пользователь может допустить подобную ошибку:
// take out a lock:
if (m_multiThreaded)
{
CSingleLock c(&m_criticalSection, TRUE);
}
// do other stuff, assuming lock is held
В конечном счете, пользователь должен понимать влияние строки кода, которую он пишет.В этом случае они должны знать, что создают объект, и знать, как долго он будет существовать.
Еще одна вероятная ошибка:
CSingleLock *c = new CSingleLock(&m_criticalSection, TRUE);
// do other stuff, don't call delete on c...
Это заставило бы вас спросить: «Могу ли я как-нибудь помешать пользователю моего класса выделить его в куче»?На что ответ будет тот же.
В C++0x будет другой способ сделать все это — использовать лямбды.Определите функцию:
template <class TLock, class TLockedOperation>
void WithLock(TLock *lock, const TLockedOperation &op)
{
CSingleLock c(lock, TRUE);
op();
}
Эта функция фиксирует правильное использование CSingleLock.Теперь позвольте пользователям сделать это:
WithLock(&m_criticalSection,
[&] {
// do stuff, lock is held in this context.
});
Пользователю гораздо сложнее облажаться.Синтаксис на первый взгляд выглядит странно, но [&] с последующим блоком кода означает: «Определите функцию, которая не принимает аргументов, и если я ссылаюсь на что-либо по имени, и это имя чего-то внешнего (например,локальная переменная в содержащей функции), позвольте мне получить к ней доступ по неконстантной ссылке, чтобы я мог ее изменить.)
Другие советы
Первый, Эрвикер делает несколько хороших замечаний -- вы не можете предотвратить каждое случайное неправильное использование этой конструкции.
Но в вашем конкретном случае этого действительно можно избежать.Это потому, что в C++ есть одно (странное) различие в отношении временных объектов: Свободные функции не могут принимать неконстантные ссылки на временные объекты. Итак, чтобы избежать внезапного появления и исчезновения блокировок, просто переместите код блокировки из CSingleLock
конструктор и в свободную функцию (которую вы можете сделать другом, чтобы не раскрывать внутренние компоненты как методы):
class CSingleLock {
friend void Lock(CSingleLock& lock) {
// Perform the actual locking here.
}
};
Разблокировка по-прежнему выполняется в деструкторе.
Использовать:
CSingleLock myLock(&m_criticalSection, TRUE);
Lock(myLock);
Да, писать немного громоздко.Но теперь компилятор будет жаловаться, если вы попытаетесь:
Lock(CSingleLock(&m_criticalSection, TRUE)); // Error! Caught at compile time.
Поскольку неконстантный параметр ref Lock()
не может привязаться к временному.
Возможно, это удивительно, но методы класса может оперировать временными протезами - вот почему Lock()
должна быть бесплатной функцией.Если вы уроните friend
спецификатор и параметр функции в верхнем фрагменте кода, чтобы сделать Lock()
метод, то компилятор с радостью позволит вам написать:
CSingleLock(&m_criticalSection, TRUE).Lock(); // Yikes!
ПРИМЕЧАНИЕ КОМПИЛЯТОРУ MS: Версии MSVC++ до Visual Studio .NET 2003 неправильно позволяли функциям привязываться к неконстантным ссылкам в версиях до VС++ 2005. Это поведение исправлено в VC++ 2005 и более поздних версиях..
Нет, нет никакого способа сделать это.Это приведет к поломке почти всего кода C++, который в значительной степени зависит от создания безымянных временных объектов.Единственное решение для конкретных классов — сделать их конструкторы частными, а затем всегда создавать их с помощью какой-то фабрики.Но я думаю, что лекарство хуже болезни!
Я так не думаю.
Хотя это и неразумно, как вы поняли из своей ошибки, в этом утверждении нет ничего «незаконного».Компилятор не имеет возможности узнать, является ли возвращаемое значение метода «жизненно важным» или нет.
ИМХО, компилятор не должен запрещать создание временных объектов.
В частности, в таких случаях, как сжатие вектора, вам действительно нужно создать временный объект.
std::vector<T>(v).swap(v);
Хотя это немного сложно, но проверка кода и модульное тестирование должны выявить эти проблемы.
В противном случае вот решение для одного бедняка:
CSingleLock aLock(&m_criticalSection); //Don't use the second parameter whose default is FALSE
aLock.Lock(); //an explicit lock should take care of your problem
А как насчет следующего?Немного злоупотребляет препроцессором, но он настолько умен, что думаю его стоит включить:
class CSingleLock
{
...
};
#define CSingleLock class CSingleLock
Теперь забываем назвать временные данные, что приводит к ошибке, потому что в C++ допустимо следующее:
class CSingleLock lock(&m_criticalSection, true); // Compiles just fine!
Тот же код, но без имени, не является:
class CSingleLock(&m_criticalSection, true); // <-- ERROR!
Я вижу, что за 5 лет никто не придумал самого простого решения:
#define LOCK(x) CSingleLock lock(&x, TRUE);
...
void f() {
LOCK(m_criticalSection);
И теперь используйте этот макрос только для создания замков.Больше нет возможности создавать временные объекты!Это имеет дополнительное преимущество: макрос можно легко расширить для выполнения любого вида проверки в отладочных сборках, например обнаружения неподходящей рекурсивной блокировки, записи файла и строки блокировки и многого другого.