Можно ли наследовать интеллектуальные указатели C ++ 11 и переопределять относительные операторы?

Согласно cppreference.com, std::shared_ptr предоставляет полный набор относительных операторов (==,! =, ‹ , ...), но семантика сравнения не указана. Я предполагаю, что они сравнивают базовые необработанные указатели с объектами, на которые есть ссылки, и что std :: weak_ptr и std :: unique_ptr делают то же самое.

Для некоторых целей я бы предпочел иметь относительные операторы, которые упорядочивают интеллектуальные указатели на основе сравнения ссылочных объектов (а не указателей на них). Я уже много чего делаю, но с моими собственными «тупыми указателями», которые ведут себя в основном как необработанные указатели, за исключением относительных операторов. Я хотел бы сделать то же самое со стандартными интеллектуальными указателями C ++ 11. Так...

  1. Можно ли наследовать интеллектуальные указатели C ++ 11 (shared_ptr, weak_ptr и unique_ptr) и переопределять относительные операторы?

  2. Есть ли какие-то скрытые проблемы, на которые мне нужно обратить внимание? Например, есть ли какие-то другие методы, которые мне нужно реализовать или использовать using, чтобы убедиться, что все работает правильно?

  3. Для максимальной лени, есть ли доступный шаблон библиотеки, который сделает это за меня автоматически?

Я надеюсь, что это "конечно, ты справишься, идиот!" вроде того, но я немного не уверен, потому что в стандартной библиотеке есть классы (по крайней мере, такие как std::map), от которых вы не должны наследовать.


person Steve314    schedule 22.09.2012    source источник
comment
В общем, наследование от чего-либо, чей деструктор не является динамическим, небезопасно. Это может быть и делается обычно, вам просто нужно быть действительно осторожным.   -  person Mooing Duck    schedule 22.09.2012
comment
@Mooing Duck - хорошо, без позднего связывания, которое может быть проблемой для деструктора и других мест - имеет смысл. Я не думаю, что это проблема для меня, по крайней мере, на данный момент, но мне нужно будет проверить. Возможно, было бы лучше обернуть умный указатель как член, а не наследовать.   -  person Steve314    schedule 22.09.2012
comment
Эээ, под динамическим я, конечно, подразумеваю виртуальный   -  person Mooing Duck    schedule 22.09.2012
comment
Я думаю, что вы подходите к этому совершенно неверно. Если у вас есть указатель на что-то и вы хотите это что-то получить, вы разыменовываете указатель. Здесь применена очень простая идея: хотите сравнить с вещами, на которые указывают умные указатели? Разыменуйте их. Умный указатель должен действовать как указатель. Напишите функцию сравнения, например dereference_compare, которая делает то, что вы хотите, разделяя концепции.   -  person GManNickG    schedule 22.09.2012
comment
Возможно, другой вариант состоит в том, что ваша цель (как вы это выразились) принимает компаратор, который будет использоваться для заказа. Например, std::set<std::shared_ptr<T>> отличается от std::set<std::shared_ptr<T>, indirect_less<std::shared_ptr<T>>>> (где indirect_less оставлено на усмотрение читателя, но должно быть очевидным).   -  person Luc Danton    schedule 22.09.2012
comment
@GManNickG - это означает, что вам нужно передать это сравнение, а также соответствующие экземпляры, и вы рискуете запутаться, когда есть два соответствующих порядка, применяя неправильный порядок к неправильным экземплярам. Обычное решение этой проблемы - упаковать две вещи вместе, чтобы они путешествовали вместе, что я и делаю.   -  person Steve314    schedule 22.09.2012
comment
@ Steve314: конечно, ты можешь это сделать, то есть добиться желаемого поведения, но не использовать наследование идиот! (Просто шучу;). Вам просто нужно определить нужный оператор с нужной семантикой - Примечание: я не утверждаю, что это хорошая идея, но на самом деле очень просто реализовать без злоупотребления наследованием и связанных с ним проблем.   -  person David Rodríguez - dribeas    schedule 22.09.2012
comment
@ Дэвид - на самом деле, идиот кажется правым - я должен был подумать о проблемах наследования, но не стал.   -  person Steve314    schedule 22.09.2012


Ответы (3)


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

template<class pointer_type>
class relative_ptr {
public:
    typedef typename std::pointer_traits<pointer_type>::pointer pointer;
    typedef typename std::pointer_traits<pointer_type>::element_type element_type;
    relative_ptr():ptr() {}
    template<class U>
    relative_ptr(U&& u):ptr(std::forward<U>(u)) {}
    relative_ptr(relative_ptr<pointer>&& rhs):ptr(std::move(rhs.ptr)) {}
    relative_ptr(const relative_ptr<pointer>& rhs):ptr(std::move(rhs.ptr)) {}

    void swap (relative_ptr<pointer>& rhs) {ptr.swap(rhs.ptr);}
    pointer release() {return ptr.release();}
    void reset(pointer p = pointer()) {ptr.reset(p);}
    pointer get() const {return ptr.get();}
    element_type& operator*() const {return *ptr;}
    const pointer_type& operator->() const {return ptr;}

    friend bool operator< (const relative_ptr& khs, const relative_ptr& rhs) const 
    {return std::less<element>(*lhs,*rhs);}
    friend bool operator<=(const relative_ptr& khs, const relative_ptr& rhs) const 
    {return std::less_equal<element>(*lhs,*rhs);}
    friend bool operator> (const relative_ptr& khs, const relative_ptr& rhs) const 
    {return std::greater<element>(*lhs,*rhs);}
    friend bool operator>=(const relative_ptr& khs, const relative_ptr& rhs) const 
    {return std::greater_equal<element>(*lhs,*rhs);}
    friend bool operator==(const relative_ptr& khs, const relative_ptr& rhs) const 
    {return *lhs==*rhs;}
    friend bool operator!=(const relative_ptr& khs, const relative_ptr& rhs) const 
    {return *lhs!=*rhs;}
protected:
    pointer_type ptr;
};

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

Я сделаю предупреждение, что мне не нравится, как работает ==, поскольку он может возвращать истину для двух указателей на разные объекты. Но как бы там ни было. Я также не тестировал код, он может не работать при выполнении определенных задач, например при попытке копирования, если он содержит unique_ptr.

person Mooing Duck    schedule 22.09.2012
comment
Вкл, поскольку он может возвращать истину для двух указателей на разные объекты - да, но то же самое относится и к обычным экземплярам. Думайте об этом как о прокси, которые просто случайно являются интеллектуальными указателями - указатель - это скорее деталь реализации, чем предоставленная абстракция. - person Steve314; 22.09.2012
comment
Я должен был сказать это раньше, но в C ++ также есть прецедент для указателей, которые действуют как прокси и не нуждаются в разыменовании - ссылки & также называются указателями саморазыменования. - person Steve314; 22.09.2012
comment
Немного более запутанный, чем то, что на самом деле нужно для этого варианта использования ... :) - person David Rodríguez - dribeas; 22.09.2012

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

namespace myns {
struct mytype {
   int value;
};
bool operator<( mytype const& lhs, mytype const& rhs ) {
   return lhs.value < rhs.value;
}
bool operator<( std::shared_ptr<mytype> const & lhs, std::shared_ptr<mytype> const & rhs )
{
   // Handle the possibility that the pointers might be NULL!!!
   // ... then ...
   return *lhs < *rhs;
}
}

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

int main() {
   std::shared_ptr<myns::mytype> a, b;
   if ( a < b ) {                       // [1]
      std::cout << "less\n";
   } else {
      std::cout << "more\n";
   }
}

В [1], и поскольку a и b являются объектами определяемыми пользователем типами (*), сработает ADL, и он добавит и std, и myns в набор поиска. Затем он найдет стандартное определение operator< для std::shared_ptr, а именно:

template<class T, class U>
bool std::operator<(shared_ptr<T> const& a, shared_ptr<U> const& b) noexcept;

И он также добавит myns и добавит:

bool myns::operator<( mytype const& lhs, mytype const& rhs );

Затем, после завершения поиска, срабатывает разрешение перегрузки, и оно определяет, что myns::operator< лучше соответствует, чем std::operator< для вызова, поскольку это идеальное совпадение, и в этом случае предпочтение отдается не шаблонам. Затем он вызовет ваш собственный operator< вместо стандартного.

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


(*) Это небольшое упрощение. Поскольку operator< может быть реализован как функция-член или как бесплатная функция, компилятор будет проверять внутри std::shared_ptr<> член operator< (отсутствует в стандарте) и друзей. Он также будет искать friend функции внутри mytype ... и так далее. Но в итоге найдет нужный.

person David Rodríguez - dribeas    schedule 22.09.2012
comment
Я делаю это, когда это уместно, но, как я указывал в различных комментариях, это больше вещей, которые нужно передать и, возможно, перепутать. Когда вы знаете, что правильно использовать две вещи вместе, упаковка их вместе гарантирует, что они путешествуют вместе, а статическая типизация обеспечивает дополнительные проверки. И хотя указатель имени может вводить в заблуждение - это скорее своего рода прокси, независимо от отсутствия сетевых проблем - это не означает, что упаковка функциональности таким образом - плохая вещь. - person Steve314; 22.09.2012
comment
@ Steve314 Я действительно не понимаю, что вы имеете в виду в своем комментарии. Бесплатные функции в пространстве имен типа являются частью одного и того же интерфейса (в C ++ интерфейс объекта не ограничен его членами из-за ADL), и это решение фактически использует его. Вам нужно только определить в том же пространстве имен, что и ваш тип (и, следовательно, в том же интерфейсе) вашу бесплатную функцию operator<. Многих путает объектно-ориентированный подход, означающий, что вы можете использовать только функции-члены, но C ++ - более богатый язык, чем этот, и это один из тех случаев, когда он сияет, вы можете расширить библиотеку. - person David Rodríguez - dribeas; 22.09.2012
comment
В этом решении ничего не нужно передавать. Операторы определены один раз в пространстве имен вашего типа, и они доступны везде, неявно, без какого-либо дополнительного кода в месте вызова. . Это не может быть проще. - person David Rodríguez - dribeas; 22.09.2012
comment
Возможно, какой-то контекст - основная причина проблемы связана с доморощенным предметно-ориентированным языком для работы с узлами AST. Одним из самых основных средств является поддержка нескольких операций диспетчеризации, но полезность ограничена использованием необработанных указателей - выполнение преобразований AST, частичная замена и частичная ссылка определенно проще, если у вас есть какая-то сборка мусора. Однако добавьте туда поддержку интеллектуальных указателей, и она должна быть везде, включая генератор полиморфного сравнения, который, возможно, предоставляет класс с обернутым указателем. - person Steve314; 22.09.2012
comment
как вы думаете, почему один класс можно заказать только в одну сторону? У меня есть реальные кейсы с множеством разных порядков. Эти упорядочения должны быть переданы в код, который поддерживает любое упорядочение, если только данные не упакованы вместе с информацией об упорядочивании, которая к ним применяется. - person Steve314; 22.09.2012
comment
@ Steve314: Нет проблем с выполнением полиморфного компаратора (ну, есть, но они не имеют отношения к рассматриваемому вопросу). Просто реализуйте operator< как виртуальную функцию-член в своем типе и предоставьте бесплатные операторы функций, которые отправляют эту функцию-член: virtual bool operator<(mytype const &); в качестве члена, затем bool operator<(std::shared_ptr<mytype> const& lhs, std::shared_ptr<mytype> const& rhs) { return *lhs < *rhs; } (как в ответе!) - person David Rodríguez - dribeas; 22.09.2012
comment
@ Steve314: Почему я думаю, что один класс может быть заказан только одним способом? Я не знаю, но вы спрашиваете об операторах отношения в shared_ptr, а там только один operator<. У вас может быть столько порядков, сколько вы хотите, но все они не могут быть operator< (ну, они могут принимать разные аргументы, но тогда вам нужно только сделать то же самое, что и с оболочкой, хотя без ловушек наследования) . То есть обертка не нужна для обеспечения функциональности, и она приносит свои собственные проблемы. - person David Rodríguez - dribeas; 22.09.2012
comment
полиморфный в данном случае означает множественную отправку, как и для операций множественной отправки в целом. Я также получаю преимущества декларативного стиля кода. - person Steve314; 22.09.2012
comment
Я могу объявить много разных оболочек на основе одного и того же базового типа, просто сделав упорядочение параметром шаблона. И ничто из этого не мешает мне напрямую использовать shared_ptr, когда я не хочу указывать какой-либо порядок. Множество различных типов со статической проверкой, чтобы убедиться, что я не путаю их, хотя я имею в виду одно и то же семейство классов узлов AST. - person Steve314; 22.09.2012
comment
@ Steve314: Нет смысла продолжать обсуждение. Просто обратите внимание, что подход оболочки не отвечает на вопрос (по крайней мере, как указано), поскольку он не влияет на то, как shared_ptr сравнивается. И я не понимаю, почему это не ответ (я не говорю, что вы его принимаете или что это наиболее подходящий дизайн для вашего домена, но это ответ на ваш вопрос) - person David Rodríguez - dribeas; 22.09.2012
comment
Оболочка в порядке - она ​​предпочитает композицию наследованию, но при этом сохраняет ссылку и порядок вместе. Отдельное сравнение даже дальше от того, что задает вопрос - я, конечно, согласен с тем, что это следует сказать, но это не тот ответ, который мне нужен. Я уже использую отдельное сравнение, когда оно является правильным решением и фактически по умолчанию все, что предоставляет этот генератор кода, - класс переноса указателя является необязательным дополнением. - person Steve314; 22.09.2012

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

Из-за этого классы, предназначенные для работы в качестве баз, обычно не должны поддерживать копирование. Когда необходимо копирование, вместо этого они должны предоставить что-то вроде Derived* clone() const override.

Проблему, которую вы пытаетесь решить, вероятно, лучше всего решить, оставив все как есть и предоставив настраиваемые компараторы при работе с такими указателями.

std::vector<std::shared_ptr<int>> ii = …;
std::sort(begin(ii), end(ii),
          [](const std::shared_ptr<int>& a, const std::shared_ptr<int>& b) {
              return *a < *b;
          });
person Marcelo Cantos    schedule 22.09.2012