Пройти lvalue в rvalue


Я создал небольшой класс "блокирующая очередь". Меня раздражает, что я создал избыточный код для значений, передаваемых в функцию-член enqueue.

Вот две функции, которые делают то же самое (за исключением того, что rvalue использует std:: move для перемещения rvalue в фактическую коллекцию очередей), за исключением дескрипторов lvalue и rvalue соответственно:

    void enqueue(const T& item)
    {
        std::unique_lock<std::mutex> lock(m);
        this->push(item);
        this->data_available = true;
        cv.notify_one();
    }

    void enqueue(T&& item)
    {
        std::unique_lock<std::mutex> lock(m);
        this->push(std::move(item));
        this->data_available = true;
        cv.notify_one();
    }

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

3 8

3 ответа:

Это классический пример необходимости идеально двигаться вперед. Сделайте это, создав шаблон функции (шаблон элемента, если это функция-член):

template <class U>
void enqueue(U&& item)
{
    std::unique_lock<std::mutex> lock(m);
    this->push(std::forward<U>(item));
    this->data_available = true;
    cv.notify_one();
}

Пояснение: Если вы передадите значение lvalue T enqueue, U выведет к T&, и forward передаст его как lvalue, и вы получите поведение копирования, которое хотите. Если вы передадите rvalue T в enqueue, U выведет к T, а forward передаст его как rvalue, и вы получите поведение перемещения, которое вы хотеть.

Это более эффективно, чем подход" pass by value", поскольку вы никогда не делаете ненужную копию или перемещение. Недостатком подхода "pass by value" является то, что функция принимает все, даже если это неправильно. Вы можете или не можете получить каскадные ошибки вниз под push. Если это вызывает беспокойство, вы можете enable_if enqueue ограничить, с какими аргументами он будет создаваться.

Обновление на основе комментария

Основываясь на приведенных ниже комментариях, это это то, что я понимаю вещи выглядят так:

#include <queue>
#include <mutex>
#include <condition_variable>

template <class T>
class Mine
    : public std::queue<T>
{
    std::mutex m;
    std::condition_variable cv;
    bool data_available = false;
public:

    template <class U>
    void
    enqueue(U&& item)
    {
        std::unique_lock<std::mutex> lock(m);
        this->push(std::forward<U>(item));
        this->data_available = true;
        cv.notify_one();
    }
};

int
main()
{
    Mine<int> q;
    q.enqueue(1);
}

Это все хорошо. Но что, если вместо этого вы попытаетесь поставить в очередь двойника:

q.enqueue(1.0);
Это все еще работает, потому что double неявно преобразуется в int. Но что, если вы не хотите, чтобы это сработало? Тогда вы можете ограничить свой enqueue следующим образом:
template <class U>
typename std::enable_if
<
    std::is_same<typename std::decay<U>::type, T>::value
>::type
enqueue(U&& item)
{
    std::unique_lock<std::mutex> lock(m);
    this->push(std::forward<U>(item));
    this->data_available = true;
    cv.notify_one();
}

Теперь:

q.enqueue(1.0);

Приводит к:

test.cpp:31:11: error: no matching member function for call to 'enqueue'
        q.enqueue(1.0);
        ~~^~~~~~~
test.cpp:16:13: note: candidate template ignored: disabled by 'enable_if' [with U = double]
            std::is_same<typename std::decay<U>::type, T>::value
            ^
1 error generated.

Но q.enqueue(1); все равно будет работать нормально. То есть ограничение вашего шаблона участника-это дизайнерское решение, которое вам нужно принять. Что U вы хотите enqueue принять? Нет правильного или неправильного ответа. Это инженерное суждение. И несколько других тестов доступны, которые могут быть более подходящими (например, std:: is_convertible, std:: is_constructible и т. д.). Возможно, правильный ответ для вашего приложения - это отсутствие ограничений вообще, как первый прототип выше.

Мне кажется, что enqueue(const&) и enqueue(&&) - это просто частный случай enqueue_emplace. Любой хороший C++11-подобный контейнер имеет эти три функции, и первые две являются частным случаем третьей.

void enqueue(const T& item) { enqueue_emplace(item); }
void enqueue(T&& item)      { enqueue_emplace(std::move(item)); }

template <typename... Args>
void enqueue_emplace(Args&&... args)
{
    std::unique_lock<std::mutex> lock(m);
    this->emplace(std::forward<Args>(args)...); // queue already has emplace
    this->data_available = true;
    cv.notify_one();
}
Это простое, но эффективное решение, и оно должно вести себя так же, как и ваш исходный интерфейс. Он также превосходит шаблонный подход, поскольку вы можете поставить в очередь список инициализаторов.

Старый пост: просто сделайте старый добрый пас по значению:

void enqueue(T item)
{
    std::unique_lock<std::mutex> lock(m);
    this->push(std::move(item));
    this->data_available = true;
    cv.notify_one();
}

enqueue "владеет" это параметр и хочет переместить его в очередь. Пройти мимо значения-вот правильное понятие, чтобы сказать это.

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

Вы смотрели на std::forward? он может просто сделать то, что вы просите, если вы добавите немного шаблонов в свою функцию...