Есть ли способ прозрачно использовать конструктор перемещения или копирования в шаблоне? - PullRequest
2 голосов
/ 29 апреля 2020

Я пытаюсь реализовать обобщенную потокобезопасную очередь c таким образом, чтобы она могла работать как с объектами только для перемещения, так и с объектами только для копирования. Вот что я попробовал (для простоты я удалил весь нерелевантный код (блокировки)):

struct MoveOnly
{
  MoveOnly() = default;
  MoveOnly(const MoveOnly& a) = delete;
  MoveOnly& operator=(const MoveOnly& a) = delete;
  MoveOnly(MoveOnly&& a) = default;
  MoveOnly& operator=(MoveOnly&& a) = default;

  std::vector<int> v;

};

struct CopyOnly
{
  CopyOnly() = default;
  CopyOnly(const CopyOnly &a) = default;
  CopyOnly &operator=(const CopyOnly &a) = default;
  CopyOnly(CopyOnly &&a) = delete;
  CopyOnly &operator=(CopyOnly &&a) = delete;

  std::vector<int> v;
};

template <typename T>
class Queue
{
  std::queue<T> q;

public:
  T pop()
  {
    T t = q.front();
    return t;
  }

  void push(T&& t)
  {
    q.push(std::forward<T>(t));
  }
};

int main()
{
  Queue<MoveOnly> qm;
  qm.push(MoveOnly());
  MoveOnly mo = qm.pop();

  Queue<CopyOnly> qc;
  CopyOnly c;
  qc.push(c);
  CopyOnly&& co = qc.pop();
}

Есть несколько ошибок компиляции из-за pop: T t = q.front() не может работать с семантикой перемещения так как функция возвращает ссылку на lvalue. T t = std::move(q.front()) не будет работать с явно удаленным конструктором перемещения, так как перегрузка оператора преобразуется в удаленный конструктор. Такая же проблема использовалась в функции push, но она решается с идеальной пересылкой.

Другая проблема, очевидно, заключается в том, что return t привязывается к конструктору перемещения для CopyOnly, и у меня возникают трудности чтобы понять, почему это так.

Есть ли способ заставить pop работать как с MoveOnly, так и с CopyOnly объектами?


Дополнительный вопрос: имеет ли смысл определять объект наподобие CopyOnly с явно удаленными конструкторами перемещения? В каком случае это будет полезно? Потому что это работает, если конструкторы неявно удаляются.

Ответы [ 2 ]

5 голосов
/ 29 апреля 2020

Вы можете использовать constexpr, если , и проверить, если T равно std::move_constructible_v.

Я бы также создал emplace прокси:

#include <type_traits>

template<typename T>
class Queue {
    std::queue<T> q;

public:
    decltype(auto) pop() {
        if constexpr(std::is_move_constructible_v<T>) {
            T t = std::move(q.front());
            q.pop();
            return t;
        } else {
            T t = q.front();
            q.pop();
            return t;
        }
    }

    template<class... Args>
    decltype(auto) emplace(Args&&... args) {
        return q.emplace(std::forward<Args>(args)...);
    }
};

Вот версия C ++ 11 (раньше я не замечал тег C ++ 11). Удаление конструктора перемещения и оператора присваивания перемещения в CopyOnly действительно приводило к путанице. Вы, вероятно, никогда не должны делать это в реальном коде.

Чтобы заставить CopyOnly co = qc.pop(); работать, pop() должен возвращать const T, иначе конструктор перемещения будет частью разрешения перегрузки, которое все равно будет даже если он удален, но компиляция не удастся только потому, что он удален.

Если для вас подходит CopyOnly&& co = qc.pop();, вы можете заменить const U на U в enable_if.

template<typename T>
class Queue {
    std::queue<T> q{};

public:
    template<typename U = T>
    typename std::enable_if<std::is_move_constructible<U>::value, U>::type
    pop() {
        U t = std::move(q.front());
        q.pop();
        return t;
    }

    template<typename U = T>
    typename std::enable_if<!std::is_move_constructible<U>::value, const U>::type
    pop() {
        U t = q.front();
        q.pop();
        return t;
    }

    template<class... Args>
    void emplace(Args&&... args) {
        q.emplace(std::forward<Args>(args)...);
    }
};

Вот еще одна версия C ++ 11, построенная на идее rafix07, с дополнительным типом popper, который делает pop после возврата, чтобы устранить возможную ошибку в g cc 7.3.

template<typename T>
class Queue {
    std::queue<T> q{};

    struct popper {
        std::queue<T>& q_ref;
        ~popper() {
            q_ref.pop();
        }
    };
public:
    using Type = typename
        std::conditional<std::is_move_constructible<T>::value, T, T&>::type;

    T pop() {
        popper pop_after_return{q};
        return std::forward<Type>(q.front());
    }

    template<class... Args>
    void emplace(Args&&... args) {
        q.emplace(std::forward<Args>(args)...);
    }
};
2 голосов
/ 29 апреля 2020

Вы можете добавить в свою очередь:

using Type = std::conditional_t< std::is_move_assignable_v<T> &&
   std::is_move_constructible_v<T>, T, T&>;

, тогда pop выглядит следующим образом:

T pop()
{
    std::decay_t<Type> t = std::forward<Type>(q.front());
    q.pop();
    return t;
}

если T является подвижным, то по std::forward<Type>(q.front()) вызывается конструктор перемещения (front приводится к значению rvalue reference для запуска построения хода). Для T как копируемого, будет выполнено копирование.

Демо

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...