Я сделал небольшой класс «очереди блокировки». Меня раздражает, что я создал избыточный код для значений, передаваемых в 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.
Это классический пример необходимости совершенной перемотки вперед. Сделайте это с помощью шаблона функции (шаблон члена, если это функция-член):
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
передаст его как значение, и вы получите желаемое поведение при движении.
Это более эффективно, чем подход «передача по значению», поскольку вы никогда не делаете ненужную копию или перемещение. Недостатком подхода «передача по значению» является то, что функция принимает все, даже если это не так. Вы можете или не можете получить каскадные ошибки ниже 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
«владеет» этим параметром и хочет переместить его в очередь. Передача по значению является правильной концепцией, чтобы сказать это.
Он делает ровно одну копию и один ход. К сожалению, это может быть медленным для T
у которых нет оптимизированного конструктора перемещения. Я думаю, что по этой причине контейнеры в стандартной библиотеке всегда имеют две перегрузки. Но с другой стороны, хороший компилятор может оптимизировать это.
Вы смотрели на std::forward
?
он может просто делать то, что вы просите, если вы добавите немного шаблонов в свою функцию …