Идеальная переадресация в C++

Что подразумевается под идеальной пересылкой?

«Пересылка» — это процесс, при котором одна функция пересылает свой параметр другой функции. В идеальном случае функция должна получить тот же объект, что и функция, выполняющая пересылку.

Другими словами, идеальная переадресация означает, что мы не просто пересылаем объекты, мы также пересылаем их существенные свойства, будь то lvalue или rvalue, const или volatile.

Не беспокойтесь слишком об определении, мы рассмотрим несколько простых примеров.

Допустим, у нас есть простой класс, как показано ниже.

class Object {
 public:
  Object() = default;

  void SetName(const std::string &name) { name_ = std::move(name); }
  std::string GetName() const { return name_; }

 private:
  std::string name_;
};

У нас также есть несколько перегруженных функций с именем UseObject

void UseObject(Object &) {
  std::cout << "calling UseObject(Object &)" << std::endl;
}

void UseObject(const Object &) {
  std::cout << "calling UseObject(const Object &)" << std::endl;
}

void UseObject(Object &&) {
  std::cout << "calling UseObject(Object &&)" << std::endl;
}

Теперь у нас есть главный

int main() {
  Object object;
  const Object const_object;
  UseObject(object);
  UseObject(const_object);
  UseObject(std::move(object));
}

который будет производить вывод, как показано ниже

calling UseObject(Object &)
calling UseObject(const Object &)
calling UseObject(Object &&)

Прямо сейчас, предположим, у нас есть простая шаблонная функция, которая пытается передать аргумент функции UseObject.

template <typename T>
void NotForwardToUseObject(T x) {
  UseObject(x);
}

Теперь запуск кода

int main() {
  Object object;
  const Object const_object;
  NotForwardToUseObject(object);
  NotForwardToUseObject(const_object);
  NotForwardToUseObject(std::move(object));
}

приведет к

calling UseObject(Object &)
calling UseObject(Object &)
calling UseObject(Object &)

где функции вызываются не так, как мы ожидали ранее.

Это связано с тем, что const и rvalueness игнорируются при вычете шаблона для void NotForwardToUseObject(T x) .

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

Теперь, если мы используем универсальную ссылку для аргумента шаблона,

template <typename T>
void HalfForwardToUseObject(T &&x) {  // universal reference
  UseObject(x);
}

Запуск кода

int main() {
  Object object;
  const Object const_object;
  HalfForwardToUseObject(object);
  HalfForwardToUseObject(const_object);
  HalfForwardToUseObject(std::move(object));
}

приведет к

calling UseObject(Object &)
calling UseObject(const Object &)
calling UseObject(Object &)

Почти! Пересылка const работает, но rvalueness аргумента все еще не пересылается правильно.

Чтобы иметь настоящую идеальную пересылку, мы должны привести x к исходному типу и lvalue- или r-value-ness.

template <typename T>
void ForwardToUseObject(T &&x) {
  UseObject(static_cast<T &&>(x));
}

Теперь запуск кода

int main() {
  Object object;
  const Object const_object;
  ForwardToUseObject(object);
  ForwardToUseObject(const_object);
  ForwardToUseObject(std::move(object));
}

приведет к

calling UseObject(Object &)
calling UseObject(const Object &)
calling UseObject(Object &&)

Идеальный! Мы успешно передали объект должным образом. Для упрощения кода мы можем использовать std::forward из библиотеки C++ <utility>,

template <typename T>
void PerfectForwardToUseObject(T &&x) {
  UseObject(std::forward<T>(x));
}

Я надеюсь, что с приведенными выше примерами вы теперь поняли, что подразумевается под идеальной переадресацией. Как обычно, приведенный выше код доступен с моего github.

Спасибо, что дочитали до конца пост!