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

165
10 ноября 2018, 03:20
#include <iostream>
#include <string>
#include <memory>
template<typename T, typename Arg>
std::shared_ptr<T> factory(const Arg& arg) {
    std::cout << "factory1\n";
    return std::shared_ptr<T>(new T(arg)); 
}
template<typename T, typename Arg>
std::shared_ptr<T> factory(Arg&& arg) {
    std::cout << "factory2\n";
    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?

Answer 1

TL;DR:

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

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

Подробнее?

Пардон за простыню текста.

Что еще за универсальные ссылки

Универсальные ссылки называются так, потому что в них одинаково хорошо можно передать и lvalue, и rvalue. А потом внутри шаблона определить, lvalue это или rvalue, и действовать соответственно.

По сути, "универсальная ссылка" - это ссылка-на-rvalue (&&), но не на любой тип, а на параметр шаблона. Но не на любой, а только на тот, который при вызове функции вы позволили компилятору определить за вас. (Иначе особые свойства универсальной ссылки пропадают, и она превращается в обычную ссылку-на-rvalue.) (Если это не параметр, а обычная переменная, или же параметр лямбды, то вместо параметра шаблона подойдет 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 &.

intом выше этого не произошло из-за кое-какой особенности встроенных типов. Однако, будь там, например, константная std::string, то тип определился бы как константный.)

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

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

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 << "lvalue\n";
    else
        std::cout << "rvalue\n";
}

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

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

Но это не сработает. После того, как rvalue ссылка создана, она сама по себе становится lvalue, и к ней нужно применять 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))); 
}
Answer 2

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

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

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

READ ALSO
Как обезопасить приложение от патча?

Как обезопасить приложение от патча?

Есть приложение которое защищено проверкой подписи, но есть патч который подменяет оригинальную подпись -> она всегда вернаПатч изменяет...

168
java hibernate sequence

java hibernate sequence

Caused by: orgpostgresql

167
Google map fragment Второй день мучаюсь с ошибкой

Google map fragment Второй день мучаюсь с ошибкой

Уже второй день пытаюсь прикрутить google карту, но все четноВ основном, брал инфу из документации google

179
Посоветуйте хороший визуализатор по java?

Посоветуйте хороший визуализатор по java?

Нашел один хороший - Sourcetrail, но он не работает(ошибка 87, не найдена java-либа, подозреваю что ему нужна eclipse-jdt)Есть еще похожие?

219