Интеллектуальные указатели C++: совместное использование указателей по сравнению с обменом данными

В этой проницательной статье один из программистов Qt пытается объяснить различные виды реализаций QT интеллектуальных указателей. В начале он делает различие между обменом данными и совместным использованием самих указателей:

Во-первых, давайте получим одну вещь прямо: существует различие между совместным использованием указателей и обменом данными. При совместном использовании указателей значение указателя и его время жизни защищено классом интеллектуального указателя. Другими словами, указатель является инвариантом. Однако объект, на который указывает указатель, полностью вне его управления. Мы не знаем, copiable ли объект или нет, если это присваиваемо или нет.

Теперь, совместное использование данных включает класс интеллектуального указателя, зная что-то о совместно используемых данных. На самом деле самое главное - то, что данные совместно используются, и мы не заботимся как. То, что указатели используются для совместного использования данных, не важно в этой точке. Например, Вы действительно не заботитесь, как классы инструмента Qt неявно совместно используются, не так ли? Что вопросы Вам то, что они совместно используются (таким образом уменьшающий потребление памяти) и что они работают, как будто они не были.

Откровенно говоря, я просто не делаю undersand это объяснение. В комментариях статьи была просьба разъяснения, но я не нашел объяснение автора достаточным.

Если Вы действительно понимаете это, объясните. Каково это различие, и как другие общие классы указателя (т.е. от повышения, или новые стандарты C++) вписывается в эту таксономию?

Заранее спасибо

9
задан Wacov 6 April 2018 в 21:03
поделиться

3 ответа

В первом случае вы добавляете уровень косвенного обращения к указателю , так что объект, представленный интеллектуальным указателем оборачивает исходный указатель. Есть только один указатель на объект, и задача оболочки - отслеживать ссылки на исходный указатель.Очень упрощенный фрагмент кода может выглядеть следующим образом:

template<typename T>
struct smart_ptr {
    T    *ptr_to_object;
    int  *ptr_to_ref_count;
};

Когда вы копируете структуру, ваш код копирования / назначения должен будет убедиться, что счетчик ссылок увеличивается (или уменьшается, если объект уничтожается), но указатель на Фактический обернутый объект никогда не изменится, и его можно просто скопировать неглубоко. Поскольку структура довольно мала, ее легко и дешево копировать, и «все», что вам нужно сделать, - это манипулировать счетчиком ссылок.

Во втором случае он больше похож на репозиторий объектов. Часть «неявно разделяемая» предполагает, что вы можете запросить у фреймворка FooWidget , выполнив что-то вроде BarFoo.getFooWidget () , и даже если он выглядит как указатель - умный или нет - то, что вы возвращаете, является указателем на новый объект, вам фактически передают указатель на существующий объект, который хранится в каком-то кеше объектов. В этом смысле он может быть больше похож на объект типа Singleton, который вы получаете, вызывая фабричный метод.

По крайней мере, для меня это различие звучит так, но я, возможно, так далек от истины, что мне понадобятся Google Maps, чтобы найти дорогу назад.

1
ответ дан 4 December 2019 в 20:22
поделиться

Допустим, у нас был этот класс

struct BigArray{
   int  operator[](size_t i)const{return m_data[i];}
   int& operator[](size_t i){return m_data[i];}
private:
   int m_data[10000000];
};

А теперь предположим, что у нас было два экземпляра:

BigArray a;
a[0]=1;//initializaation etc
BigArray b=a;

На этом этапе нам нужен этот инвариант

assert(a[0]==b[0]);

Копия по умолчанию ctor обеспечивает этот инвариант, однако за счет глубокого копирования всего объекта. Мы можем попытаться ускорить подобное

struct BigArray{
   BigArray():m_data(new int[10000000]){}
   int  operator[](size_t i)const{return (*m_data)[i];}
   int& operator[](size_t i){return (*m_data)[i];}
private:
   shared_ptr<int> m_data;
};

. Это также будет соответствовать инварианту, без создания глубокой копии, так что пока все хорошо. Теперь, используя эту новую реализацию, которую мы сделали

b[0]=2;

Теперь мы хотим, чтобы это было работают так же, как и случай глубокого копирования assert (a [0]! = b [0]); Но это не удается. Чтобы решить эту проблему, нам нужно небольшое изменение:

struct BigArray{
       BigArray():m_data(new int[10000000]){}
       int  operator[](size_t i)const{return (*m_data)[i];}
       int& operator[](size_t i){
          if(!m_data.unique()){//"detach"
            shared_ptr<int> _tmp(new int[10000000]);
            memcpy(_tmp.get(),m_data.get(),10000000);
            m_data=_tmp;
          }
          return (*m_data)[i];
       }
    private:
       shared_ptr<int> m_data;
    };

Теперь у нас есть класс, который неглубоко копируется, когда нужен только константный доступ, и глубоко копируется, когда нужен неконстантный доступ. Это идея, лежащая в основе концепции указателя shared_data. Вызовы const не будут выполнять глубокое копирование (они называют это «отсоединением»), в то время как неконстантные вызовы будут выполнять глубокое копирование, если они будут совместно использоваться. Он также добавляет некоторую семантику поверх operator ==, чтобы сравнивать не только указатель, но и данные, чтобы это работало:

BigArray b=a;//shallow copy
assert(a==b);//true
b[0]=a[0]+1;//deep copy
b[0]=a[0];//put it back
assert(a==b);//true

Этот метод называется COW (копирование при записи) и применяется с тех пор. рассвет C ++. Он также чрезвычайно хрупкий - приведенный выше пример, кажется, работает, потому что он маленький и имеет несколько вариантов использования.На практике это редко стоит проблем, и на самом деле в C ++ 0x не рекомендуется использовать строки COW. Так что используйте с осторожностью.

3
ответ дан 4 December 2019 в 20:22
поделиться

В более позднем комментарии он немного проясняет этот вопрос

Это важный момент, который я пытался прояснить в первом разделе. Когда вы используете QSharedPointer, вы разделяете право собственности на указатель. Класс управляет и обрабатывает только указатель - все остальное (например, доступ к данным) выходит за его пределы. Когда вы используете QSharedDataPointer, вы делитесь данными. И этот класс предназначен для неявного разделения: поэтому он может разделиться.

Попытка интерпретировать это:

Что важно увидеть, так это то, что «указатель» в данном случае не означает объект, хранящий адрес, а означает место хранения, в котором находится объект (сам адрес). Строго говоря, я думаю, вы должны сказать, что делитесь адресом. boost :: shared_ptr , таким образом, является интеллектуальным указателем, совместно использующим «указатель». boost :: intrusive_ptr или другой навязчивый интеллектуальный указатель, похоже, также разделяет указатель, хотя и знает что-то об объекте, на который указывает (что у него есть член счетчика ссылок или функции, увеличивающие / уменьшающие его).

Пример: если кто-то поделился с вами черным ящиком, и он не знает, что находится в черном ящике, это похоже на совместное использование указателя (который представляет собой ящик), но не данных (что находится внутри ящика). ). Фактически, вы даже не можете знать, что то, что находится внутри коробки, можно разделить (что, если коробка вообще ничего не содержит?).Интеллектуальные указатели представлены вами и другим парнем (и вы, конечно, не разделены), но адрес - это ящик, и он является общим.

Совместное использование данных означает, что интеллектуальный указатель знает достаточно данных, на которые указывает, чтобы он мог изменить адрес, на который указывает (и это необходимо для копирования данных и т. Д.). Итак, теперь указатели могут указывать на разные адреса. Поскольку адрес другой, адрес больше не используется. То же самое и std :: string в некоторых реализациях:

std::string a("foo"), b(a);
 // a and b may point to the same storage by now.
std::cout << (void*)a.c_str(), (void*)b.c_str();
 // but now, since you could modify data, they will
 // be different
std::cout << (void*)&a[0], (void*)&b[0];

Совместное использование данных не обязательно означает, что вам предоставлен указатель. Вы можете использовать std :: string только с помощью a [0] и cout << a; и никогда не касаться любого из c_str () функций. Тем не менее, обмен может происходить за кулисами. То же самое происходит со многими классами Qt и классами других наборов инструментов виджетов, что называется неявным совместным использованием (или копией при записи ). Поэтому я думаю, что можно резюмировать это так:

  • Совместное использование указателя: мы всегда указываем на один и тот же адрес, когда копируем интеллектуальный указатель, подразумевая, что мы разделяем значение указателя.
  • Обмен данными: мы можем указывать на разные адреса в разное время. Подразумевается, что мы умеем копировать данные с одного адреса на другой.

Итак, попытка категоризации

  • boost :: shared_ptr , boost :: intrusive_ptr : делитесь указателем, а не данными.
  • QString , QPen , QSharedDataPointer : поделиться данными, которые он содержит.
  • std :: unique_ptr , std :: auto_ptr (а также QScopedPointer ): ни указатель, ни данные не используются совместно.
7
ответ дан 4 December 2019 в 20:22
поделиться
Другие вопросы по тегам:

Похожие вопросы: