Умные указатели C ++: совместное использование указателей или обмен данными - PullRequest
9 голосов
/ 17 апреля 2010

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

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

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

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

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

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

Ответы [ 3 ]

7 голосов
/ 17 апреля 2010

В последующем комментарии он немного прояснил вопрос

Это важный момент, который я пытался объяснить в первом разделе. Когда вы используете 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): ни общий указатель, ни данные.
3 голосов
/ 17 апреля 2010

Скажем, у нас был этот класс

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;

Теперь мы хотим, чтобы это работало так же, как и в случае с глубокой копией. утверждают (а [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 вызовы не будут выполнять глубокое копирование (они называют это «отсоединением»), в то время как неконстантные вызовы будут выполнять глубокое копирование, если оно используется совместно. Он также добавляет некоторую семантику поверх оператора ==, чтобы сравнивать не только указатель, но и данные, чтобы это работало:

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. Так что используйте с осторожностью.

1 голос
/ 17 апреля 2010

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

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

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

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

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

...