выведены конфликтующие типы для параметра «T» для универсальной ссылки

Я тестирую универсальную ссылку со следующим кодом:

template <typename T>
vector<T> attach_(vector<T> xs, T&& x) {
  xs.push_back(std::forward<T>(x));
  return xs;
}

int main() {
   int k = 2;
   attach_(std::move(vector<int>{1,2,3}),k);          //not OK
   attach_(std::move(vector<int>{1,2,3}),(int&)k);    //not OK
   attach_(std::move(vector<int>{1,2,3}),(int)k);     //OK
   attach_(std::move(vector<int>{1,2,3}),2);          //OK
}

и получил ошибку:

no matching function for call to 'attach_(std::remove_reference<std::vector<int> >::type, int&)'
attach_(std::move(vector<int>{1,2,3}),k);
note:   template argument deduction/substitution failed:
note:   deduced conflicting types for parameter 'T' ('int' and 'int&')
   attach_(std::move(vector<int>{1,2,3}),k);

У SO есть вопрос по аналогичной ошибке Сообщение об ошибке выводит конфликтующие типы для параметр 'const T' о константных ссылках.

Я также протестировал несколько других случаев, некоторые с преобразованием типов. Некоторые работают, а другие нет.

Я слышал, что универсальные ссылки, такие как T&&, подходят ко всему. Почему здесь не получается?

Второй вопрос: как ввести attach_, чтобы убедиться, что семантика перемещения работает как для xs, так и для x для соответствующего ввода? В конечном итоге я хотел бы иметь вариант следующего:

for(int i = 0; i < 100; i++)
   xs = attach_(xs,values[i])

работать, не делая ненужных копий.

(Это проверено с помощью gcc4.8.1 с использованием g++ -std=c++11 test.cpp)

Спасибо

-- РЕДАКТИРОВАТЬ ---

Спасибо всем за отличные ответы.

Итак, теперь я понимаю, что в этом случае эффективно просто использовать передачу по значению и переместить T. Предположим, что вектор xs не копируется без необходимости при передаче параметров и возврате обратно, если он используется в цикле, верно?

Я задал связанный с этим вопрос Когда ссылка на константу лучше, чем передача по значению в C++11?. Там у меня был такой пример, когда все говорили, что проход через долину — плохая идея:

int hd(vector<int> a) {
   return a[0];
}

Можно ли вообще использовать универсальную ссылку для обработки как случая hd, так и случая attach_ в этом посте, чтобы избежать ненужных копий?

Спасибо еще раз.

--- РЕДАКТИРОВАТЬ2 ---

Итак, я протестировал версии в ответах, а также справочную версию const ниже. Оптимизация не используется для выявления потенциальных проблем. Версия const ref является худшей, так как она принудительно копирует. Все остальное имеет ту же скорость, если для вектора используется std::move(a), за исключением необработанных вызовов push_call быстрее. Я думаю, оптимизация может устранить эту разницу. Я предполагаю, что тест (или, может быть, тип int) недостаточно велик, чтобы показать разницу между push_back(x) и push_back(std::move(x))

#include <vector>
#include <iostream>
#include <chrono>
using namespace std;

template <class T>
vector<T> attach(vector<T> v, T x) {
  v.push_back(x);
  return v;
}

template <typename T>
vector<T> attach1(vector<T> xs, T x) {
  xs.push_back(std::move(x));
  return xs;
}

template <typename T, typename E = typename std::remove_reference<T>::type>
std::vector<E> attach2(std::vector<E> xs, T&& x) {
  xs.push_back(std::forward<T>(x));
  return xs;
}

template <typename C, typename T> C attach3(C&& xs, T&& x) {
  xs.push_back(std::move<T>(x));
  return std::forward<C>(xs);
}

template <class T>
vector<T> attach4(const vector<T>& v, T x) {
  vector<T> ret = v;
  ret.push_back(x);
  return std::move(ret);
}

using namespace std::chrono;
int main() {
  int N = 100000;
  vector<int> a;
  auto time = high_resolution_clock::now();
  for (int i = 0; i < N; i++) {
    //a.push_back(i);    //0s
    //a = attach(a,i);    //15s
    //a = attach(std::move(a),i);    //0.03s
    //a = attach2(std::move(a),i);   //0.03s
    a = attach3(std::move(a),i);   //0.03s
    //a = attach4(std::move(a),i);   //14.9s
  }
  cout << duration_cast<duration<double>>(high_resolution_clock::now() - time).count() << endl;

}

person tinlyx    schedule 08.07.2014    source источник
comment
У вас нет универсальной ссылки. UR должен быть только одним единственным аргументом функции.   -  person Kerrek SB    schedule 08.07.2014
comment
Ага, понятно. Таким образом, мы не только не можем добавить const и тому подобное в T&&, но мы не можем иметь никаких других типов, включающих T?   -  person tinlyx    schedule 08.07.2014
comment
Это уничтожило бы весь смысл, не так ли...   -  person Kerrek SB    schedule 08.07.2014
comment
Я все еще учусь этому. Смотрел ролик Скотта за час про два амперсанда. Почему люди не использовали ` или что-то подобное для правильной ссылки, а вместо этого придумали все эти правила и особые случаи?   -  person tinlyx    schedule 08.07.2014


Ответы (3)


Универсальные ссылки работают следующим образом: если вы передаете rvalue, то T будет выведено как int (или какой-либо другой нессылочный тип), потому что тогда T&& является ссылочным типом rvalue. Но если вы передаете lvalue, то T будет выведено как int& (или какой-либо другой ссылочный тип lvalue), потому что тогда T&& будет ссылочным типом lvalue (поскольку ссылка lvalue и ссылка rvalue «схлопываются» вместе в ссылку lvalue ).

Итак, в случае, если вы передаете lvalue, у вас есть проблема, потому что вы не можете иметь vector<T>, когда T является ссылочным типом.

Вы должны просто пройти по значению,

template <typename T>
std::vector<T> attach_(std::vector<T> xs, T x) {
  xs.push_back(std::move(x));
  return xs;
}

Это может выглядеть менее эффективным, но это не так. Если вы передадите значение r, оно будет перемещено один раз в x и еще раз перемещено в вектор. Если вы передадите lvalue, оно будет скопировано один раз в x, а затем перемещено в вектор. Это то же самое, как если бы вы передавали по ссылке: одна копия для lvalue, ноль копий для rvalue.

В образовательных целях вы можете сделать это с помощью своей универсальной ссылки:

template <typename T, typename E = typename std::remove_reference<T>::type>
std::vector<E> attach_(std::vector<E> xs, T&& x) {
  xs.push_back(std::forward<T>(x));
  return xs;
}

Это гарантирует, что когда вы передаете lvalue, тип элемента вектора является нессылочным типом. Но действительно лучше просто передать по значению.

person Brian Bi    schedule 08.07.2014
comment
Если мы идем по второму пути, то решение Deduplicator лучше. Параметр E в этом случае всегда будет выводиться из типа вектора, а не использовать параметр по умолчанию. - person T.C.; 08.07.2014
comment
@Т.С. Да, наверное, это лучшее решение. - person Brian Bi; 08.07.2014

Я слышал, что универсальные ссылки, такие как T&&, подходят ко всему. Почему здесь не получается?

Причина, по которой универсальные ссылки соответствуют всем, заключается в том, что правила вывода аргументов шаблона говорят, что когда T && сочетается с lvalue типа X, T выводится как X&, а затем свертывание ссылок превращает X& && в X&.

В attach_(std::move(vector<int>{1,2,3}),k); компилятор выводит T как int из первого параметра (vector<T> ‹-> vector<int>), а из второго параметра T выводит как int &, поскольку k является lvalue. Таким образом, вы получаете ошибку.

Второй вопрос: как ввести attach_, чтобы убедиться, что семантика перемещения работает как для xs, так и для x для соответствующего ввода?

Проще всего было бы просто передать его по значению и переместить в вектор:

template <typename T>
vector<T> attach_(vector<T> xs, T x) {
  xs.push_back(std::move(x));
  return xs;
}

x по-прежнему будет создаваться с помощью перемещения, если оно передано с std::move.


Редактировать: если вы работаете с большим устаревшим типом только для копирования, то создание двух копий, как в приведенном выше случае, не идеально. В этом случае вы можете сделать то, что @Deduplicator показал в своем ответе:

template <typename T>
vector<typename std::remove_reference<T>::type>
attach_(vector<typename std::remove_reference<T>::type> xs, T&& x) {
  xs.push_back(std::forward<T>(x));
  return xs;
}

Редактировать 2:

Итак, теперь я понимаю, что в этом случае эффективно просто использовать передачу по значению и переместить T. Предположим, что вектор xs не копируется без необходимости при передаче параметров и возврате обратно, если он используется в цикле, верно?

Общее правило таково: «передавать по значению, если оно маленькое или если вам все равно нужно сделать копию, а в противном случае — по ссылке». В attach_ вам необходимо сделать копию x (путем push_backвставки ее в вектор), поэтому можно передать ее по значению, а затем переместить.

Следует ли передавать вектор по значению, зависит от предполагаемой семантики. Если attach_(xs, x) не должен изменять xs, то вам необходимо сделать копию вектора, чтобы он возвращался в любом случае, и поэтому вы должны передавать его по значению. Однако, когда вы сделаете xs = attach_(xs, x);, вы получите копию. xs = attach_(std::move(xs), x); не требует копирования, но имеет небольшие дополнительные накладные расходы из-за построения перемещения, за которым следует назначение перемещения.

Если attach_(xs, x) должен изменить xs, то передать его по неконстантной ссылке. Никаких накладных расходов.

Можно ли вообще использовать универсальную ссылку для обработки как случая hd, так и случая attach_ в этом посте, чтобы избежать ненужных копий?

hd не нуждается в универсальной ссылке. Вы просто индексируете вектор, поэтому просто передайте его по константной ссылке.

person T.C.    schedule 08.07.2014

Универсальная эталонная семантика работает согласно этой цитате:

8.3.2 Ссылки §6

Если typedef-name (7.1.3, 14.1) или decltype-specifier (7.1.6.2) обозначают тип TR, который является ссылкой на тип T, попытка создать тип «ссылка lvalue на cv TR» создает тип «ссылка lvalue на T», а попытка создать тип «ссылка rvalue на cv TR» создает тип TR. [ Пример:

int i;
typedef int& LRI;
typedef int&& RRI;
LRI& r1 = i; // r1 has the type int&
const LRI& r2 = i; // r2 has the type int&
const LRI&& r3 = i; // r3 has the type int&
RRI& r4 = i; // r4 has the type int&
RRI&& r5 = 5; // r5 has the type int&&
decltype(r2)& r6 = i; // r6 has the type int&
decltype(r2)&& r7 = i; // r7 has the type int&

— конец примера]

Разумное использование std::remove_reference легко решает вашу ошибку:

#include <vector>
using namespace std;

template <typename T>
vector<typename std::remove_reference<T>::type>
attach_(vector<typename std::remove_reference<T>::type> xs, T&& x) {
  xs.push_back(std::forward<T>(x));
  return xs;
}

int main() {
   int k = 2;
   attach_(std::move(vector<int>{1,2,3}),k);          //now OK
   attach_(std::move(vector<int>{1,2,3}),(int&)k);    //now OK
   attach_(std::move(vector<int>{1,2,3}),(int)k);     //OK
   attach_(std::move(vector<int>{1,2,3}),2);          //OK
}

В любом случае, вы уверены, что хотите передать контейнер по значению?
Кроме того, почему бы не дать ему собственный параметр шаблона, чтобы сделать функцию более универсальной?

template <typename T, typename C> void attach_(C& xs, T&& x) {
    xs.push_back(std::forward<T>(x));
}

Возвращать контейнер больше не нужно...

person Deduplicator    schedule 08.07.2014
comment
Два аргумента шаблона, безусловно, подходят, IMO. Однако в вашем последнем примере неправильный тип возвращаемого значения. - person Ben Voigt; 08.07.2014
comment
Последний пример также требует серьезного изменения того, как он в настоящее время использует функцию (он дает rvalue в качестве первого аргумента). - person M.M; 08.07.2014
comment
@MattMcNabb: Да, у него другая подпись. В этом смысл последнего раздела, дать ему подпись, которая имеет больше смысла, что может означать изменение того, как он используется. - person Deduplicator; 08.07.2014