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

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
Martin Castin 29 Апр 2020 в 11:50

2 ответа

Лучший ответ

Вы можете использовать constexpr if и проверить, является ли 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 после возврата, чтобы устранить возможную ошибку в gcc 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)...);
    }
};
5
Ted Lyngmo 29 Апр 2020 в 14:22

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

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 преобразуется в ссылку на значение для запуска построения перемещения). Для T, который можно скопировать, будет выполнено копирование.

демонстрация

2
rafix07 29 Апр 2020 в 09:22