
Уже год в свободное от работы время я пилю что-то вроде смеси Maven и Spring для С++. Важной её частью является самописная система умных указателей. Зачем мне всё это — отдельная тема. В данной статье я хочу коротко рассказать о том, как одна, казалось бы, полезная фича С++ заставила меня усомниться в здравом смысле Стандарта.
Редактировано:
Приношу свои извинения хабрасообществу и Стандарту. Буквально на следующий день после отправки статьи осознал грубую ошибку в своих размышлениях. Лучше читать сразу конец статьи… и, да, к copy elision, выходит, статья относиться лишь косвенно.
1. Проблема
Умные указатели для проекта были сделаны ещё летом прошлого года.
Избранный код указателей и пояснения
У нас есть структуры-стратегии, которые реализуют логику хранения объекта и логику доступа к объекту. Мы передаём их типы в качестве шаблонных аргументов класса умного указателя. IDSharedMemoryHolder — интерфейс доступа к память объекта. Через вызов функции retain() умный указатель начинает владеть объектом (для strong reference-а ++ref_count). По вызову release() указатель освобождает объект (для strong reference-а --ref_count и удаление объекта если ref_count == 0).
Я намеренно опустил здесь вещи, связанные с разыменованием и с retain по вызовам операторов. Описываемая проблема этих моментов не касается.
template<typename T_Type, typename T_Holding, typename T_Access>
class DReference {
. . .
IDSharedMemoryHolder *_holder;
void retain(IDSharedMemoryHolder *inHolder) { . . . }
void release() { . . . }
. . .
~DReference() { release(); }
template<typename T_OtherType,
typename T_OtherHolding,
typename T_OtherAccess>
DReference(
const DReference<T_OtherType, T_OtherHolding, T_OtherAccess> &
inLReference) : _holder(NULL), _holding(), _access()
{
retain(inLReference._holder);
}
. . .
}
У нас есть структуры-стратегии, которые реализуют логику хранения объекта и логику доступа к объекту. Мы передаём их типы в качестве шаблонных аргументов класса умного указателя. IDSharedMemoryHolder — интерфейс доступа к память объекта. Через вызов функции retain() умный указатель начинает владеть объектом (для strong reference-а ++ref_count). По вызову release() указатель освобождает объект (для strong reference-а --ref_count и удаление объекта если ref_count == 0).
Я намеренно опустил здесь вещи, связанные с разыменованием и с retain по вызовам операторов. Описываемая проблема этих моментов не касается.
Работа умных указателей проверялась рядом простых тестов: «создали объект, связанный с указателем — присвоили указателю указатель — глянули, чтобы reatin/release прошли правильно». Тесты (что сейчас кажется очень странным) проходили. Код на умные указатели я перевёл в начале января и… да, тогда всё тоже работало.
Проблемы начались месяц назад, когда обнаружилось, что память, контролируемая умными указателями, удалялась раньше времени.
Поясню на конкретном примере:
DStrongReference<DPlugIn> DPlugInManager::createPlugIn(
const DPlugInDescriptor &inDescriptor)
{
. . .
DStrongReference<DPlugIn> thePlugInReference =
internalCreatePlugIn(inDescriptor);
. . .
return thePlugInReference;
}
...
DStrongReference<DPlugIn> DPlugInManager::internalCreatePlugIn(
const DPlugInDescriptor &inDescriptor)
{
for (IDPlugInStorage *thePlugInStorage : _storages) {
if (thePlugInStorage->getPlugInStatus(inDescriptor))
return thePlugInStorage->createPlugIn(inDescriptor);
}
return DStrongReference<DPlugIn>();
}
...
class DPlugInStorageImpl : public IDPlugInStorage {
public:
virtual ~DPlugInStorageImpl() { }
virtual DStrongReference<DPlugIn> createPlugIn(
const DPlugInDescriptor &inDescriptor);
};
При вызове метода DPlugInStorageImpl::createPlugIn(...) создавался объект, возвращаемый через DStrongReference, после чего этот умный указатель возвращался через метод DPlugInManager::internalCreatePlugIn(...) в контекст вызова — метод DPlugInManager::createPlugIn(...).
Так вот, когда умный указатель возвращался в метод DPlugInManager::createPlugIn(...), thePlugInReference указывал на удалённый объект. Очевидно, дело было в неправильном количестве retain/release-вызовов. Потратив кучу нервов с дебаггером в Eclipse (к слову — он ужасен), я плюнул, и решил проблему по-простому — использовал лог. Поставил на вызовах методов retain и release вывод, запустил программу… Что я ожидал увидеть? Вот такое что-то (псевдокод):
DPlugInStorageImpl::createPlugIn(...) => RETAIN
DPlugInManager::internalCreatePlugIn(...), return createPlugIn => RETAIN
DPlugInStorageImpl::createPlugIn(...), ~DStrongReference() => RELEASE
DPlugInManager::createPlugIn(...), thePlugInReference = internalCreatePlugIn(...) => RETAIN
DPlugInManager::internalCreatePlugIn(...),~DStrongReference() => RELEASE
Итого: ref_count = 1 для thePlugInReference. Всё должно было быть чётко.
То, что я увидел на самом деле, заставило меня сделать вот так (0_0) и потратить следующие
DPlugInStorageImpl::createPlugIn(...) => RETAIN
DPlugInManager::internalCreatePlugIn(...),~DStrongReference() => RELEASE
Отчаявшись решить проблему в боевом коде и уже подозревая что-то крайне неладное, я создал маленький тестовый проект.
2. Тест
Тестовый код:
#include <iostream>
#include <stdio.h>
class TestClass {
private:
int _state;
public:
TestClass(int inState) : _state(inState) { std::cout << "State based: " << inState << std::endl; }
TestClass() : _state(1) { std::cout << "Default" << std::endl; }
TestClass(const TestClass &inObject0) : _state(2) { std::cout << "Const Copy" << std::endl; }
TestClass(TestClass &inObject0) : _state(3) { std::cout << "Copy" << std::endl; }
TestClass(const TestClass &&inObject0) : _state(4) { std::cout << "Const Move" << std::endl; }
TestClass(TestClass &&inObject0) : _state(5) { std::cout << "Move" << std::endl; }
~TestClass() { std::cout << "Destroy" << std::endl; }
void call() { std::cout << "Call " << _state << std::endl; }
};
///////////////////////////////////////////////////////////////////////////////
int main() {
TestClass theTestObject = TestClass();
theTestObject.call();
fflush(stdout);
return 0;
}
Ожидаемый результат:
Default
Const Copy
Call 1
Destroy
Реальный результат:
Default
Call 1
Destroy
То есть копи-конструктор не вызывался. И только тогда я сделал то, что нужно было сделать сразу. Загуглил и узнал про copy_elision.
3. Страшная правда
В дв��х словах — любой компилятор С++ может без предупреждения и каких-либо флагов игнорировать вызов copy-конструктора и вместо этого, например, напрямую копировать полное состояние объекта. При этом выполнить какую-либо логику в процессе такого копирования
Оптимизация — это, конечно, отлично… Но что если мне нужно выполнять какую-нибудь логику в конструкторе копирования. Например, для умных указателей? И что мне до сих пор непонятно, почему нельзя было разрешить выполнять подобную оптимизацию при -o1 в случае, если в теле copy-конструктора нет никакой логики?.. До сих пор сие мне не ясно.
4. Решение
Я нашёл два способа заставить компилятор выполнять логику в момент конструирования объектов класса:
1) Через флаги компиляции. Плохой способ. Компиляторозависимый. Например, для g++ нужно ставить флаг -fno-elide-constructors, и это либо будет влиять на весь проект (что ужасно), либо придётся использовать в соответствующих местах push/pop настройки флагов компилятора, что загромождает код и делает менее читабельным (особенно с учётом того, что такое придётся делать под каждый компилятор).
2) Через ключевое слово explicit. Это тоже плохой способ, но, на мой взгляд, это лучше, че�� использовать флаги компиляции.
Спецификатор explicit нужен, чтобы запретить неявное создание экземпляров класса через синтаксис приведения типов. То есть, для того, чтобы вместо MyInt theMyInt = 1 нужно было обязательно писать MyInt theMyInt = MyInt(1).
В случае, если выставить это слово перед copy-конструктором, мы получим достаточно забавный запрет неявного приведения типа — запрет приведения к своему типу.
Таким образом, например, следующий
код
#include <iostream>
#include <stdio.h>
class TestClass {
private:
int _state;
public:
TestClass(int inState) : _state(inState) { std::cout << "State based: " << inState << std::endl; }
TestClass() : _state(1) { std::cout << "Default" << std::endl; }
explicit TestClass(const TestClass &inObject0) : _state(2) { std::cout << "Const Copy" << std::endl; }
}
~TestClass() { std::cout << "Destroy" << std::endl; }
void call() { std::cout << "Call " << _state << std::endl; }
};
///////////////////////////////////////////////////////////////////////////////
int main() {
TestClass theTestObject = TestClass();
theTestObject.call();
fflush(stdout);
return 0;
}
у меня (g++ 4.6.1) вызвал ошибку:
error: no matching function for call to 'TestClass::TestClass(TestClass)'
Что ещё забавнее, из-за особенностей синтаксиса С++ вот так: TestClass theTestObject(TestClass()) записать тоже не выйдет, ведь это будет считаться объявлением указателя на функцию и вызовет ошибку:
error: request for member 'call' in 'theTestObject', which is of non-class type 'TestClass(TestClass (*)())'
Таким образом, мы вместо того, чтобы заставить компилятор выполнять конструктор копирования, запретили вызывать этот конструктор.
К счастью для меня, такое решение подошло. Дело в том, что запретив конструктор копирования я вынудил компилятор использовать спецификацию шаблонного конструктора с теми же шаблонными аргументами, что и у текущего класса. То есть это не было «приведение объекта к своему типу», а это было «приведение к типу, у которого те же шаблонные аргументы», что порождает ещё один метод, но заменяет конструктор копирования.
Вот что вышло
template<typename T_Type, typename T_Holding, typename T_Access>
class DReference {
. . .
IDSharedMemoryHolder *_holder;
void retain(IDSharedMemoryHolder *inHolder) { . . . }
void release() { . . . }
. . .
~DReference() { release(); }
//NB: Workaround for Copy elision
explicit DReference(
const OwnType &inLReference)
: _holder(NULL), _holding(), _access()
{
// Call for some magic cases
retain(inLReference._holder);
}
template<typename T_OtherType,
typename T_OtherHolding,
typename T_OtherAccess>
DReference(
const DReference<T_OtherType, T_OtherHolding, T_OtherAccess> &
inLReference) : _holder(NULL), _holding(), _access()
{
retain(inLReference._holder);
}
. . .
}
Для тестового примера аналог этого костыля выглядел бы вот так:
Тестовый код и короткое пояснение
Та же фишка. Спецификация шаблона, заменяющая конструктор копирования… Тут видно, что это плохое решение, ведь мы не к месту использовали шаблоны. Если кто знает как лучше — отпишитесь.
#include <iostream>
#include <stdio.h>
class TestClass {
private:
int _state;
public:
TestClass(int inState) : _state(inState) { std::cout << "State based: " << inState << std::endl; }
TestClass() : _state(1) { std::cout << "Default" << std::endl; }
explicit TestClass(const TestClass &inObject0) : _state(2) { std::cout << "Const Copy" << std::endl; }
template<typename T>
TestClass(const T &inObject0) : _state(13) { std::cout << "Template Copy" << std::endl; }
~TestClass() { std::cout << "Destroy" << std::endl; }
void call() { std::cout << "Call " << _state << std::endl; }
};
///////////////////////////////////////////////////////////////////////////////
int main() {
TestClass theTestObject = TestClass();
theTestObject.call();
fflush(stdout);
return 0;
}
Та же фишка. Спецификация шаблона, заменяющая конструктор копирования… Тут видно, что это плохое решение, ведь мы не к месту использовали шаблоны. Если кто знает как лучше — отпишитесь.
Вместо заключения
Когда я рассказал о copy elision нескольким знакомым, которые года три в С++ и около-С++ разработке, они
Надеюсь, данная статья кому-нибудь пригодится и сэкономит чьё-нибудь время.
П.С.: Пишите по поводу замеченных оплошностей — буду править.
Редактировано:
Комментирующие правы, я вообще неправильно понял проблему. Спасибо всем, особенно Monnoroch за выявление логических ошибок в статье.
Написав следующий тестовый код, я получил корректный output:
Template-класс с copy-конструктором и output
Output:
///////////////////////////////////////////////////////////////////////////////
#include <iostream>
///////////////////////////////////////////////////////////////////////////////
template<typename T_Type>
class TestTemplateClass {
private:
typedef TestTemplateClass<T_Type> OwnType;
T_Type _state;
public:
TestTemplateClass() : _state() {
std::cout << "Default constructor" << std::endl;
}
TestTemplateClass(int inState) : _state(inState) {
std::cout << "State constructor" << std::endl;
}
TestTemplateClass(const OwnType &inValue) {
std::cout << "Copy constructor" << std::endl;
}
template<typename T_OtherType>
TestTemplateClass(const TestTemplateClass<T_OtherType> &inValue) {
std::cout << "Template-copy constructor" << std::endl;
}
template<typename T_OtherType>
void operator = (const TestTemplateClass<T_OtherType> &inValue) {
std::cout << "Operator" << std::endl;
}
~TestTemplateClass() {
std::cout << "Destructor" << std::endl;
}
};
///////////////////////////////////////////////////////////////////////////////
TestTemplateClass<int> createFunction() {
return TestTemplateClass<int>();
}
///////////////////////////////////////////////////////////////////////////////
int main() {
TestTemplateClass<int> theReference = createFunction();
std::cout << "Finished" << std::endl;
return 0;
}
///////////////////////////////////////////////////////////////////////////////
Output:
Default constructor
Copy constructor
Destructor
Copy constructor
Destructor
Finished
Destructor
То есть действительно, проблема была не в copy elision и не нужно никаких хаков.
Действительная же ошибка оказалась банальной. Сейчас стыдно за то, что я взялся писать статью, не проверив всё как следует.
Дело в том, что умные указатели принимают три шаблонных аргумента:
template<typename T_Type, typename T_Holding, typename T_Access>
class DReference {
. . .
- T_Type — тип объекта, который управляется системой умных указателей.
- T_Holding — стратегия владения памятью.
- T_Access — стратегия доступа к памяти.
Такой вариант реализации умных указателей позволяет сделать гибкой настройку их поведения, но при этом делает громоздким их использование (особенно с учётом того, что стратегии также шаблонные классы).
Пример объявления strong-указателя:
DReference<MyType, DReferenceStrongHolding<MyType>, DReferenceCachingAccess< MyType > > theReference;
Чтобы избежать загромождения кода, я хотел использовать фичу стандар��а С++11 — template-alias. Но, как оказалось, g++ 4.6.1 их не поддерживает. Конечно, когда пишешь свой домашний pet-project возиться с настройкой среды лень, поэтому я решил сделать очередной workaround и избавиться от аргумента с помощью наследования:
template<typename T_Type>
class DStrongReference : public DReference< T_Type, DReferenceStrongHolding<MyType>, DReferenceCachingAccess< MyType > > {
. . .
При этом нужно было определить кучу конструкторов для DStrongReference, вызывающих из себя соответствующие конструкторы базового класса DReference — ведь конструкторы не наследуются. И, конечно, я пропустил конструктор копирования… В общем, единственный совет, который могу дать после всех этих приключений — нужно быть очень внимательным при использовании шаблонов, чтобы не попасть в такую глупую ситуацию, в которую попал я.
P.S.: Вот тест, который использует наследование для замены template-alias (спасибо ToSHiC за дельный совет передавать this в output):
Тестовая имитация template-alias
Output:
///////////////////////////////////////////////////////////////////////////////
#include <iostream>
///////////////////////////////////////////////////////////////////////////////
template<typename T_Type, typename T_Strategy>
class TestTemplateClass {
private:
typedef TestTemplateClass<T_Type, T_Strategy> OwnType;
T_Type _state;
T_Strategy _strategy;
public:
TestTemplateClass() : _state(), _strategy() {
std::cout << "Default constructor: " << this << std::endl;
}
TestTemplateClass(int inState) : _state(inState), _strategy() {
std::cout << "State constructor: " << this << std::endl;
}
TestTemplateClass(const OwnType &inValue)
: _state(), _strategy()
{
std::cout << "Copy constructor: " << this << " from " <<
&inValue << std::endl;
}
template<typename T_OtherType, typename T_OtherStrategy>
TestTemplateClass(
const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
: _state(), _strategy()
{
std::cout << "Template-copy constructor: " << this << std::endl;
}
void operator = (const OwnType &inValue) {
std::cout << "Assigning: " << this << " from " << inValue << std::endl;
}
template<typename T_OtherType, typename T_OtherStrategy>
void operator = (
const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
{
std::cout << "Assigning: " << this << " from "
<< &inValue << std::endl;
}
~TestTemplateClass() {
std::cout << "Destructor: " << this << std::endl;
}
};
///////////////////////////////////////////////////////////////////////////////
template<typename T_Type>
class TestTemplateClassIntStrategy : public TestTemplateClass<T_Type, int> {
private:
//- Types
typedef TestTemplateClassIntStrategy<T_Type> OwnType;
typedef TestTemplateClass<T_Type, int> ParentType;
public:
TestTemplateClassIntStrategy() : ParentType() { }
TestTemplateClassIntStrategy(int inState) : ParentType(inState) { }
TestTemplateClassIntStrategy(const OwnType &inValue)
: ParentType(inValue) { }
template<typename T_OtherType, typename T_OtherStrategy>
TestTemplateClassIntStrategy(
const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
: ParentType(inValue) { }
//- Operators
void operator = (const OwnType &inValue) {
ParentType::operator =(inValue);
}
template<typename T_OtherType, typename T_OtherStrategy>
void operator = (
const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
{
ParentType::operator =(inValue);
}
};
///////////////////////////////////////////////////////////////////////////////
TestTemplateClassIntStrategy<int> createFunction() {
return TestTemplateClassIntStrategy<int>();
}
int main() {
TestTemplateClassIntStrategy<int> theReference = createFunction();
std::cout << "Finished" << std::endl;
return 0;
}
Output:
Default constructor: 0x28fed8
Copy constructor: 0x28ff08 from 0x28fed8
Destructor: 0x28fed8
Copy constructor: 0x28ff00 from 0x28ff08
Destructor: 0x28ff08
Finished
Destructor: 0x28ff00
Вызов оператора присвоения
. . .
int main() {
TestTemplateClassIntStrategy<int> theReference;
theReference = createFunction();
std::cout << "Finished" << std::endl;
return 0;
}
Default constructor: 0x28ff00
Default constructor: 0x28fed8
Copy constructor: 0x28ff08 from 0x28fed8
Destructor: 0x28fed8
Assigning: 0x28ff00 from 0x28ff08
Destructor: 0x28ff08
Finished
Destructor: 0x28ff00
Важный минус этого способа: если таким образом определить strong-pointer и weak-pointer, они будут совершенно разных типов (не связанных даже с одним template-классом) и не выйдет их присваивать один другому напрямую в момент инициализации.
<Редактировано №2>
Снова поторопился что-то утверждать. Ночью понял. Выйдет ведь… У этих классов один общий шаблонный предок.
То есть если в наследнике (который имитирует template-alias) будет описан шаблонный конструктор от произвольного DReference, в следующем коде всё будет нормально:
Код
DStrongReference<Type> theStrongReference;
// В следующей строке будет вызван шаблонный конструктор от базового шаблонного класса. Вот этот:
//
// template<typename Type, typename Owning, typename Holding>
// DWeakReference::DWeakReference(const DReference<Type, Owning, Holding> &ref) : Parent(ref) { }
//
// Этот конструктор будет вызван за счёт того, что DStrongReference наследует DReference.
//
DWeakReference<Type> theWeakReference = theStrongReference;
Тестовый код для двух классов, организованных таким образом:
template-alias через наследование: два псевдо-alias
Output:
//============================================================================
// Name : demiurg_application_example.cpp
// Author :
// Version :
// Copyright : Your copyright notice
// Description : Hello World in C++, Ansi-style
//============================================================================
///////////////////////////////////////////////////////////////////////////////
#include <iostream>
///////////////////////////////////////////////////////////////////////////////
template<typename T_Type, typename T_Strategy>
class TestTemplateClass {
private:
typedef TestTemplateClass<T_Type, T_Strategy> OwnType;
T_Type _state;
T_Strategy _strategy;
public:
TestTemplateClass() : _state(), _strategy() {
std::cout << "Default constructor: " << this << std::endl;
}
TestTemplateClass(int inState) : _state(inState), _strategy() {
std::cout << "State constructor: " << this << std::endl;
}
TestTemplateClass(const OwnType &inValue)
: _state(), _strategy()
{
std::cout << "Copy constructor: " << this << " from " <<
&inValue << std::endl;
}
template<typename T_OtherType, typename T_OtherStrategy>
TestTemplateClass(
const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
: _state(), _strategy()
{
std::cout << "Template-copy constructor: " << this << std::endl;
}
void operator = (const OwnType &inValue) {
std::cout << "Assigning: " << this << " from " << &inValue << std::endl;
}
template<typename T_OtherType, typename T_OtherStrategy>
void operator = (
const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
{
std::cout << "Assigning: " << this << " from "
<< &inValue << std::endl;
}
~TestTemplateClass() {
std::cout << "Destructor: " << this << std::endl;
}
};
///////////////////////////////////////////////////////////////////////////////
//- Integer strategy
template<typename T_Type>
class TestTemplateClassIntStrategy : public TestTemplateClass<T_Type, int> {
private:
//- Types
typedef TestTemplateClassIntStrategy<T_Type> OwnType;
typedef TestTemplateClass<T_Type, int> ParentType;
public:
TestTemplateClassIntStrategy() : ParentType() { }
TestTemplateClassIntStrategy(int inState) : ParentType(inState) { }
TestTemplateClassIntStrategy(const OwnType &inValue)
: ParentType(inValue) { }
template<typename T_OtherType, typename T_OtherStrategy>
TestTemplateClassIntStrategy(
const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
: ParentType(inValue) { }
//- Operators
void operator = (const OwnType &inValue) {
ParentType::operator =(inValue);
}
template<typename T_OtherType, typename T_OtherStrategy>
void operator = (
const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
{
ParentType::operator =(inValue);
}
};
//- Boolean strategy
template<typename T_Type>
class TestTemplateClassBoolStrategy : public TestTemplateClass<T_Type, bool> {
private:
//- Types
typedef TestTemplateClassBoolStrategy<T_Type> OwnType;
typedef TestTemplateClass<T_Type, bool> ParentType;
public:
TestTemplateClassBoolStrategy() : ParentType() { }
TestTemplateClassBoolStrategy(int inState) : ParentType(inState) { }
TestTemplateClassBoolStrategy(const OwnType &inValue)
: ParentType(inValue) { }
template<typename T_OtherType, typename T_OtherStrategy>
TestTemplateClassBoolStrategy(
const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
: ParentType(inValue) { }
//- Operators
void operator = (const OwnType &inValue) {
ParentType::operator =(inValue);
}
template<typename T_OtherType, typename T_OtherStrategy>
void operator = (
const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue)
{
ParentType::operator =(inValue);
}
};
///////////////////////////////////////////////////////////////////////////////
TestTemplateClassBoolStrategy<int> createFunction() {
return TestTemplateClassBoolStrategy<int>();
}
int main() {
TestTemplateClassIntStrategy<int> theReference;
theReference = createFunction();
std::cout << "Finished" << std::endl;
return 0;
}
Output:
Default constructor: 0x28fed8
Copy constructor: 0x28ff08 from 0x28fed8
Destructor: 0x28fed8
Copy constructor: 0x28ff00 from 0x28ff08
Destructor: 0x28ff08
Finished
Destructor: 0x28ff00
В общем, всё работает
</Редактировано №2>
Спасибо 1eqinfinity, Torvald3d за указание орфографических ошибок.