Взялся я за создание смартпоинтера, реализующего copy-on-write и в процессе наткнулся на непонятные для себя грабли. Почему-то компилятор совершенно игнорирует наличие константного перегруженного оператора разыменования, в результате чего рушится вся концепция копирования при записи. Приведу пример максимально упрощенного кода, показывающего проблему:
#include <iostream>
class A {
public:
A(int value) : _value(value) { }
const int &operator *() const
{
std::cout << "const int &A::operator *() const" << std::endl;
return _value;
}
int &operator *()
{
std::cout << "int &A::operator *()" << std::endl;
return _value;
}
private:
int _value;
};
int main()
{
A a = 10;
int v = *a;
return 0;
}
При запуске программа выводит "int &A::operator *()", а должна на мой взгляд выводить "const int &A::operator *() const". Подскажите, где моя ошибка? Я даже пробовал сделать
Здравствуйте, theCreature, Вы писали:
C>Взялся я за создание смартпоинтера, реализующего copy-on-write и в процессе наткнулся на непонятные для себя грабли. Почему-то компилятор совершенно игнорирует наличие константного перегруженного оператора разыменования, в результате чего рушится вся концепция копирования при записи. Приведу пример максимально упрощенного кода, показывающего проблему:
Хорошо известная беда, ещё со времён попытки сделать COW в Dinkumware STL для VC7, в std::string.
Дело в том, что в С++ вывод типов и разрешение сигнатур — однонаправленное (почти всюду, об этом ниже).
Компилятор смотрит на тип операнда: неконстантный, ага, вызываем неконстантный метод.
Ну и что, что полученный результат позже не изменяется. Компилятор такими тонкостями семантики не обременяется.
Соответственно, все методы доступа — *, ->, [] — оголяющие содержимое, создают угрозу для того, что содержимое поменяется извне, и должны поэтому заранее противостоять этой угрозе.
Что же делать?
А вот что. Отдавать не голое содержимое, а обёртку с семантикой ссылки. С явно перегруженными операторами модификации.
Эскиз:
struct Pointer
{
int* ptr_;
// намеренно дал разные имена, чтобы не морочиться с (const Pointer) в точке вызоваint const* get_const() const { return ptr_; }
int* get_variable() { ensure_unique(); return ptr_; }
Reference operator* () { return Reference(this); }
int const& operator* () const { return *get_const(); }
};
struct Reference
{
Pointer* host_;
Reference(Pointer* host) : host_(host) {}
// утилитыint const& get_const() const { return *host_->get_const(); }
int& get_variable() const { return *host_->get_variable(); } // приводит к COW
// чтение...operator int const& () const { return get_const(); }
operator int& () const { return get_variable(); } // явное получение неконстантной голой ссылки
// трактуется как покушение на изменения
// прямое изменение...int& operator = (int v) { return get_variable() = v; }
// прочие +=, -=, и т.д. не нужны, так как они определены над int&
// поэтому Reference(...) += 123 вызовет get_variable()
};
Код нерабочий, т.к. два класса ссылаются друг на друга. Это лечится или вынесением определения функций за пределы классов, или шаблонами, или вложением одного класса в другой... Но, чтобы не было лишнего синтаксического шума, оставляю эскиз как есть.
К>Дело в том, что в С++ вывод типов и разрешение сигнатур — однонаправленное (почти всюду, об этом ниже).
К>Что же делать?
К>А вот что. Отдавать не голое содержимое, а обёртку с семантикой ссылки. С явно перегруженными операторами модификации.
Забыл раскрыть тему про обратный вывод.
Единственное место, когда компилятор смотрит не только на тип источника (в данном случае, на Reference Pointer::operator*), но и на тип приёмника — это вызов пользовательского оператора приведения типа.
void foo(int const&); // приёмник номер один - безопасныйvoid bar(int&); // приёмник номер два - опасный
....
Pointer ptr;
foo(*ptr);
bar(*ptr); // нужно сделать упреждающее COW
*ptr = 123; // а здесь изменение явное, надо делать COW
*ptr += 456; // а здесь - почти явное, - фактически, этоoperator+=(*ptr,456); // мало чем отличается от bar(*ptr)
Упреждающее COW делаем в operator int&(), отличая его от безопасного operator const int&()
Вот поэтому нам и потребовался промежуточный тип Reference — чтобы двояко перегрузить оператор приведения.
Понятное дело, что у голого int перегружать мы ничего не можем.
Здравствуйте, Кодт, Вы писали:
C>>Взялся я за создание смартпоинтера, реализующего copy-on-write и в процессе наткнулся на непонятные для себя грабли. Почему-то компилятор совершенно игнорирует наличие константного перегруженного оператора разыменования, в результате чего рушится вся концепция копирования при записи. Приведу пример максимально упрощенного кода, показывающего проблему: К>Хорошо известная беда, ещё со времён попытки сделать COW в Dinkumware STL для VC7, в std::string. К>Дело в том, что в С++ вывод типов и разрешение сигнатур — однонаправленное (почти всюду, об этом ниже). К>Компилятор смотрит на тип операнда: неконстантный, ага, вызываем неконстантный метод. К>Ну и что, что полученный результат позже не изменяется. Компилятор такими тонкостями семантики не обременяется. К>Соответственно, все методы доступа — *, ->, [] — оголяющие содержимое, создают угрозу для того, что содержимое поменяется извне, и должны поэтому заранее противостоять этой угрозе. К>Что же делать? К>А вот что. Отдавать не голое содержимое, а обёртку с семантикой ссылки. С явно перегруженными операторами модификации.
Это кстати пример того что итераторы всё-таки не говно
Здравствуйте, Vain, Вы писали:
К>>А вот что. Отдавать не голое содержимое, а обёртку с семантикой ссылки. С явно перегруженными операторами модификации. V>Это кстати пример того что итераторы всё-таки не говно
Ну, если быть честным, то дело не в итераторах — как сущностях с функциями разыменования и навигации.
Точно так же, MySuperSmartString::operator[] может возвращать ReferenceToChar, безо всяких итераторов.
Здравствуйте, Кодт, Вы писали:
К>>>А вот что. Отдавать не голое содержимое, а обёртку с семантикой ссылки. С явно перегруженными операторами модификации. V>>Это кстати пример того что итераторы всё-таки не говно
. К>Ну, если быть честным, то дело не в итераторах — как сущностях с функциями разыменования и навигации. К>Точно так же, MySuperSmartString::operator[] может возвращать ReferenceToChar, безо всяких итераторов.
Ну возвращать хоть какой-то прокси всё-равно придётся.
[In theory there is no difference between theory and practice. In
practice there is.]
[Даю очевидные ответы на риторические вопросы]
Кстати насколько я понял это решение не идеально. Возващенный proxy-объект прекрасно работает в случае простых типов, но если смартпоинтер указывает на класс, то появляются дополнительные ограничения. Предположим, что внутри у нас не int, а std::string (я буду исходить из того, что Pointer — это нечто, похожее на ваш эскиз). Тогда при попытке вызвать на месте функцию член, произойдет ошибка:
Pointer ptr; // Смартпоинтер для std::string
(*ptr).c_str(); // В данном случае *ptr возвращает тип Reference
// Преобразования типа здесь компилятору делать не нужно,
// поэтому эта строка вызывает ошибку
Здравствуйте, theCreature, Вы писали:
C>Кстати насколько я понял это решение не идеально. Возващенный proxy-объект прекрасно работает в случае простых типов, но если смартпоинтер указывает на класс, то появляются дополнительные ограничения. Предположим, что внутри у нас не int, а std::string (я буду исходить из того, что Pointer — это нечто, похожее на ваш эскиз). Тогда при попытке вызвать на месте функцию член, произойдет ошибка
Естественно, подход имеет ограничения.
И, как мне кажется, выход состоит в том, чтобы не гоняться за неявностью лишний раз. Хочешь модифицировать содержимое — вызови функцию, дающую неконстантный доступ.
Например, *p — всегда константный, p.var() — неконстантный.
И прокси не понадобятся, и поведение будет предсказуемое.
Здравствуйте, Кодт, Вы писали:
К>Например, *p — всегда константный, p.var() — неконстантный. К>И прокси не понадобятся, и поведение будет предсказуемое.
+100!
Только я бы не метод делал, а свободную функцию. Например GetWritable( p )->xxx();
1) Не будет путанницы где мы методы указателя зовём, а где методы оказуемого
2) Для умных указателей, реализующих разные стратегии владения можно иметь перегруженный функции. В том числе и для простого указателя.
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, theCreature, Вы писали:
C>Взялся я за создание смартпоинтера,
Если делаешь эмулятор встроенного C++ поинтера, то operator*() const у него возращает T* — не T *const;
Пример.
int i;
int* const c_ptr = &i; // декларируем константный указатель. Не путать с указателем на константу.int* m_ptr = &(*c_ptr); // в твоем случае компилятор ругнется тут