Пройти 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 ответа:
Это классический пример необходимости идеально двигаться вперед. Сделайте это, создав шаблон функции (шаблон элемента, если это функция-член):
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
Это более эффективно, чем подход" pass by value", поскольку вы никогда не делаете ненужную копию или перемещение. Недостатком подхода "pass by value" является то, что функция принимает все, даже если это неправильно. Вы можете или не можете получить каскадные ошибки вниз подT
enqueue
,U
выведет кT&
, иforward
передаст его как lvalue, и вы получите поведение копирования, которое хотите. Если вы передадите rvalueT
вenqueue
,U
выведет кT
, аforward
передаст его как rvalue, и вы получите поведение перемещения, которое вы хотеть.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); }
Это все хорошо. Но что, если вместо этого вы попытаетесь поставить в очередь двойника:
Это все еще работает, потому что double неявно преобразуется в int. Но что, если вы не хотите, чтобы это сработало? Тогда вы можете ограничить свойq.enqueue(1.0);
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
s, которые не имеют оптимизированного конструктора перемещения. Я думаю, что по этой причине контейнеры в стандартной библиотеке всегда имеют две перегрузки. Но с другой стороны, хороший компилятор может оптимизировать это.
Вы смотрели на
std::forward
? он может просто сделать то, что вы просите, если вы добавите немного шаблонов в свою функцию...