TransWikia.com

Шаблонная factory функция

Stack Overflow на русском Asked by pier_nasos on December 19, 2021

#include <iostream>
#include <string>
#include <memory>

template<typename T, typename Arg>
std::shared_ptr<T> factory(const Arg& arg) {
    std::cout << "factory1n";
    return std::shared_ptr<T>(new T(arg)); 
}

template<typename T, typename Arg>
std::shared_ptr<T> factory(Arg&& arg) {
    std::cout << "factory2n";
    return std::shared_ptr<T>(new T(std::move(arg))); 
}

int main()
{
    auto sp1 = factory<std::string>(std::string("AAAAA"));
    std::string s("BBBBBBB");
    auto sp2 = factory<std::string>(s);
}

output:

factory2
factory2

Шаблонные функции взяты из примеров ю-туб лекции по с++11. Второй вызов factory должен был привести к вызову второй функции, то есть, в консоли ожидалось:

factory2
factory1

Почему всегда вызывается factory2? Как вызвать factory1?

2 Answers

TL;DR:

У вас во второй функции параметр - не обычная rvalue-ссылка, а так называемая пробрасывающая ссылка1, которая одинаково хорошо обрабатывает и lvalue, и rvalue.

В этом случае она сработала как std::string &. А этот вариант подходит лучше, чем const std::string &, так что вызывается вторая функция.

1 — Англ. "forwarding reference", иногда еще переводят как "передаваемая ссылка". Менее распространено другое название, "universal reference" - универсальная ссылка.


Что еще за пробрасывающие ссылки

Их иногда называют "универсальными" - потому их одинаково хорошо можно инициализировать выражениями не только любого типа, но и любой категории: lvalue или rvalue. С информацией о категории дальше можно делать что угодно, но обычно ее используют, чтобы передать ("пробросить") ссылку куда-то еще с сохранением оригинальной категории. Такая передача называется perfect forwarding - "идеальная передача".

По сути, "пробрасывающая ссылка" - это rvalue-ссылка (&&), но не на любой тип, а на параметр шаблона, который при вызове функции вы позволили компилятору определить за вас. (Иначе особые свойства пробрасывющей ссылки пропадают, и она превращается в обычную rvalue-ссылку.)
Также, вместо параметра шаблона подходит auto - если ссылка является отдельной переменной или параметром лямбды. (В C++20 добавился краткий способ записи шаблонов функций, позволяющий использовать auto также в параметрах обычных функций.)


Почему это работает

Почему такая ссылка принимает и lvalue тоже, хотя, казалось бы, должна работать только с rvalue?

Сначала нужно рассказать про...


Правила схлопывания ссылок

Думаю понятно, что так писать нельзя: int & && - ссылок на ссылки не бывает, компилятор запрещает их создавать.

Однако, что есть написать так?

using A = int &;
A &&my_reference = что-нибудь;

Этот вариант скомпилируется.

my_reference будет иметь тип int &. Почему? Из-за этих самых правил схлопывания ссылок.

Если вы пытаетесь создать ссылку на ссылку (не напрямую, ведь это ошибка, а через usingи или параметры шаблонов), то вид ссылки, которая получится: на lvalue или на rvalue - определяется по этим правилам:

&  + &  -> &
&& + &  -> &
&  + && -> &
&& + && -> &&

Почему именно так? Понятия не имею; слышал, что эти правила специально подгоняли под пробрасывющие ссылки... Подробнее ниже.


Определение аргумента шаблона по пробрасывющей ссылке

Казалось бы, какой смысл в правилах схлопывания ссылок? Вот какой:

Если вы пытаетесь передать в пробрасывющую ссылку rvalue, то аргумент шаблона определяется компилятором по обычным правилам:

template <typename T> void foo(T &&) {}

foo(1); // T = int

С этим все понятно, и никаких вопросов не возникает.

Однако, если передать lvalue, вот так:

template <typename T> void foo(T &&) {}

foo(1); // T = int

int x = 1;
foo(x); // T = int &

То шаблонный аргумент будет определен как lvalue-ссылка. Далее, по правилам схлопывания ссылок весь параметр функции становится lvalue-ссылкой.

Если передавать константный объект, то тип будет определен, соответственно, как const T или const T &.

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

В этом примере:

std::string s("BBBBBBB");
factory<std::string>(s);

Параметр функции, благодаря пробрасывающей ссылке, получит тип std::string &.

А это более подходящий тип, чем const std::string &, так что выбирается вариант с пробрасывающей ссылкой.


Как пользоваться пробрасывающими ссылками

Все удобство в том, что теперь мы внутри шаблона можем определить, что нам передали - lvalue или rvalue. И на основе этого решить, например, делать std::move или нет.

Как именно определить? По тому, определился ли параметр шаблона как ссылка:

template <typename T> void foo(T &&)
{
    if (std::is_reference_v<T>) // Или is_lvalue_reference, без разницы.
        std::cout << "lvaluen";
    else
        std::cout << "rvaluen";
}

Для справки: Можно подумать, что проверка на lvalue/rvalue - бесполезное занятие, и попробовать написать так:

template <typename T, typename Arg> std::shared_ptr<T> factory(Arg &&arg)
{
    return std::shared_ptr<T>(new T(arg)); 
}

Но это не сработает. Rvalue-ссылки, как и любые переменные, сами являются lvalue (хотя и ссылаются на rvalue), и к ним нужно применять std::move. Такой вот курьез языка.

Ладно, а как на основе этого сделать условный std::move? Наивный вариант выглядит так:

template <typename T, typename Arg> std::shared_ptr<T> factory(Arg &&arg)
{
    if (std::is_reference_v<T>)
        return std::shared_ptr<T>(new T(arg)); 
    else
        return std::shared_ptr<T>(new T(std::move(arg))); 
}

Однако, есть вариант проще. В стандартной библиотеке уже есть все необходимое:

template <typename T, typename Arg> std::shared_ptr<T> factory(Arg &&arg)
{
    return std::shared_ptr<T>(new T(std::forward<Arg>(arg))); 
}

std::forward - это, по сути, условный std::move. Если его шаблонный параметр - это lvalue ссылка, то он ничего не делает. А иначе он работает как std::move.

Если не нравится стандартная библиотека, можно даже так:

template <typename T, typename Arg> std::shared_ptr<T> factory(Arg &&arg)
{
    return std::shared_ptr<T>(new T((Arg &&)arg)); 
}

Эффект точно такой же. Это работает из-за правил схлопывания ссылок, описанных выше.


Итог

Таким образом, вам не нужно две перегрузки.

Достаточно одной всеядной:

template <typename T, typename Arg> std::shared_ptr<T> factory(Arg &&arg)
{
    return std::shared_ptr<T>(new T(std::forward<Arg>(arg))); 
}

Answered by HolyBlackCat on December 19, 2021

factory(Arg&& arg) это «всеядный» вызов, который в обоих случаях лучше первого варианта. Для второго случая, чтобы вызвать первый вариант, нужно добавить к аргументу const, а для второго варианта — ничего. Поэтому второй побеждает. Если Вы определите свой объект так:

const std::string s("BBBBBBB");

То победит первый вариант.

Answered by ixSci on December 19, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP