Перегрузка оператора разыменования
От: theCreature  
Дата: 14.08.10 17:23
Оценка:
Взялся я за создание смартпоинтера, реализующего 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". Подскажите, где моя ошибка? Я даже пробовал сделать
int v = const_cast<const int &>(*a);

но безрезультатно.
Re: Перегрузка оператора разыменования
От: wander  
Дата: 14.08.10 17:54
Оценка:
Здравствуйте, theCreature, Вы писали:

C>
C>int main()
C>{
C>    const A a = 10;
C>    int v = *a;

C>    return 0;
C>}
C>


Выделенное смотри.
С чего по-твоему тут должен был работать константный оператор?
Re: Перегрузка оператора разыменования
От: Анатолий Широков СССР  
Дата: 14.08.10 17:57
Оценка: 2 (1)
A a = 10;
int v = *static_cast<const A&>(a);
Re[2]: Перегрузка оператора разыменования
От: wander  
Дата: 14.08.10 18:12
Оценка:
Здравствуйте, Анатолий Широков, Вы писали:

АШ>
АШ>A a = 10;
АШ>int v = *static_cast<const A&>(a);
АШ>


Можно так, без статик_каста:
template <typename T>
T const & const_value(T const & a)
{
    return a;
}

A a = 10;
int v = *const_value(a);
Re: Перегрузка оператора разыменования
От: Кодт Россия  
Дата: 14.08.10 18:43
Оценка: 4 (2) +1
Здравствуйте, 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()
};

Код нерабочий, т.к. два класса ссылаются друг на друга. Это лечится или вынесением определения функций за пределы классов, или шаблонами, или вложением одного класса в другой... Но, чтобы не было лишнего синтаксического шума, оставляю эскиз как есть.
Перекуём баги на фичи!
Re[2]: Перегрузка оператора разыменования
От: Кодт Россия  
Дата: 14.08.10 18:52
Оценка:
К>Дело в том, что в С++ вывод типов и разрешение сигнатур — однонаправленное (почти всюду, об этом ниже).

К>Что же делать?


К>А вот что. Отдавать не голое содержимое, а обёртку с семантикой ссылки. С явно перегруженными операторами модификации.


Забыл раскрыть тему про обратный вывод.
Единственное место, когда компилятор смотрит не только на тип источника (в данном случае, на 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 перегружать мы ничего не можем.
Перекуём баги на фичи!
Re[3]: Перегрузка оператора разыменования
От: theCreature  
Дата: 14.08.10 20:19
Оценка:
Спасибо за пояснения
Re[2]: Перегрузка оператора разыменования
От: Vain Россия google.ru
Дата: 15.08.10 07:35
Оценка:
Здравствуйте, Кодт, Вы писали:

C>>Взялся я за создание смартпоинтера, реализующего copy-on-write и в процессе наткнулся на непонятные для себя грабли. Почему-то компилятор совершенно игнорирует наличие константного перегруженного оператора разыменования, в результате чего рушится вся концепция копирования при записи. Приведу пример максимально упрощенного кода, показывающего проблему:

К>Хорошо известная беда, ещё со времён попытки сделать COW в Dinkumware STL для VC7, в std::string.
К>Дело в том, что в С++ вывод типов и разрешение сигнатур — однонаправленное (почти всюду, об этом ниже).
К>Компилятор смотрит на тип операнда: неконстантный, ага, вызываем неконстантный метод.
К>Ну и что, что полученный результат позже не изменяется. Компилятор такими тонкостями семантики не обременяется.
К>Соответственно, все методы доступа — *, ->, [] — оголяющие содержимое, создают угрозу для того, что содержимое поменяется извне, и должны поэтому заранее противостоять этой угрозе.
К>Что же делать?
К>А вот что. Отдавать не голое содержимое, а обёртку с семантикой ссылки. С явно перегруженными операторами модификации.
Это кстати пример того что итераторы всё-таки не говно
Автор: MasterZiv
Дата: 26.07.10
.
[In theory there is no difference between theory and practice. In
practice there is.]
[Даю очевидные ответы на риторические вопросы]
Re[3]: Перегрузка оператора разыменования
От: Кодт Россия  
Дата: 15.08.10 08:27
Оценка:
Здравствуйте, Vain, Вы писали:

К>>А вот что. Отдавать не голое содержимое, а обёртку с семантикой ссылки. С явно перегруженными операторами модификации.

V>Это кстати пример того что итераторы всё-таки не говно
Автор: MasterZiv
Дата: 26.07.10
.


Ну, если быть честным, то дело не в итераторах — как сущностях с функциями разыменования и навигации.
Точно так же, MySuperSmartString::operator[] может возвращать ReferenceToChar, безо всяких итераторов.
Перекуём баги на фичи!
Re[4]: Перегрузка оператора разыменования
От: Vain Россия google.ru
Дата: 15.08.10 09:22
Оценка:
Здравствуйте, Кодт, Вы писали:

К>>>А вот что. Отдавать не голое содержимое, а обёртку с семантикой ссылки. С явно перегруженными операторами модификации.

V>>Это кстати пример того что итераторы всё-таки не говно
Автор: MasterZiv
Дата: 26.07.10
.

К>Ну, если быть честным, то дело не в итераторах — как сущностях с функциями разыменования и навигации.
К>Точно так же, MySuperSmartString::operator[] может возвращать ReferenceToChar, безо всяких итераторов.
Ну возвращать хоть какой-то прокси всё-равно придётся.
[In theory there is no difference between theory and practice. In
practice there is.]
[Даю очевидные ответы на риторические вопросы]
Re[3]: Перегрузка оператора разыменования
От: theCreature  
Дата: 16.08.10 11:11
Оценка:
Кстати насколько я понял это решение не идеально. Возващенный proxy-объект прекрасно работает в случае простых типов, но если смартпоинтер указывает на класс, то появляются дополнительные ограничения. Предположим, что внутри у нас не int, а std::string (я буду исходить из того, что Pointer — это нечто, похожее на ваш эскиз). Тогда при попытке вызвать на месте функцию член, произойдет ошибка:
Pointer ptr;     // Смартпоинтер для std::string
(*ptr).c_str();  // В данном случае *ptr возвращает тип Reference
                 // Преобразования типа здесь компилятору делать не нужно,
                 // поэтому эта строка вызывает ошибку
Re[4]: Перегрузка оператора разыменования
От: Кодт Россия  
Дата: 16.08.10 12:37
Оценка: +1
Здравствуйте, theCreature, Вы писали:

C>Кстати насколько я понял это решение не идеально. Возващенный proxy-объект прекрасно работает в случае простых типов, но если смартпоинтер указывает на класс, то появляются дополнительные ограничения. Предположим, что внутри у нас не int, а std::string (я буду исходить из того, что Pointer — это нечто, похожее на ваш эскиз). Тогда при попытке вызвать на месте функцию член, произойдет ошибка


Естественно, подход имеет ограничения.

И, как мне кажется, выход состоит в том, чтобы не гоняться за неявностью лишний раз. Хочешь модифицировать содержимое — вызови функцию, дающую неконстантный доступ.
Например, *p — всегда константный, p.var() — неконстантный.
И прокси не понадобятся, и поведение будет предсказуемое.
Перекуём баги на фичи!
Re[5]: Перегрузка оператора разыменования
От: Erop Россия  
Дата: 17.08.10 05:14
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Например, *p — всегда константный, p.var() — неконстантный.

К>И прокси не понадобятся, и поведение будет предсказуемое.

+100!
Только я бы не метод делал, а свободную функцию. Например GetWritable( p )->xxx();
1) Не будет путанницы где мы методы указателя зовём, а где методы оказуемого
2) Для умных указателей, реализующих разные стратегии владения можно иметь перегруженный функции. В том числе и для простого указателя.
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re: Перегрузка оператора разыменования
От: dimchick Украина  
Дата: 31.08.10 06:49
Оценка:
Здравствуйте, theCreature, Вы писали:

C>Взялся я за создание смартпоинтера,


Если делаешь эмулятор встроенного C++ поинтера, то operator*() const у него возращает T* — не T *const;

Пример.

int i;
int* const c_ptr = &i; // декларируем константный указатель. Не путать с указателем на константу.
int* m_ptr = &(*c_ptr); // в твоем случае компилятор ругнется тут
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.