Выборочная функция пересылки

Задача состоит в том, чтобы создать функцию с одним аргументом, которая перенаправляет все типы, кроме одного (Foo), который она преобразует (в Bar).

(Предположим, что существует преобразование из Foo в Bar).

Вот сценарий использования:

template<typename Args...>
void f( Args... args )
{
g( process<Args>(args)... );
}

(Я пытался извлечь / упростить его из исходного контекста Вот. — если я ошибся, подскажите пожалуйста кто-нибудь!)

Вот две возможные реализации:

template<typename T>
T&& process(T&& t) {
return std::forward<T>(t);
}

Bar process(Foo x) {
return Bar{x};
}

А также…

template <typename T, typename U>
T&& process(U&& u) {
return std::forward<T>(std::forward<U>(u));
}

template <typename T>
Bar process(Foo x) {
return Bar{x};
}

У меня это на хорошем авторитете (Вот) что второй предпочтительнее.

Однако я не могу понять данное объяснение. Я думаю, что это углубление в некоторые из самых темных уголков C ++.

Я думаю, что мне не хватает техники, необходимой, чтобы понять, что происходит. Может ли кто-нибудь объяснить подробно? Если это слишком много копать, кто-нибудь может порекомендовать ресурс для изучения необходимых предпосылок концепции?

РЕДАКТИРОВАТЬ: Я хотел бы добавить, что в моем конкретном случае сигнатура функции будет соответствовать одному из typedef-s на эта страница. То есть каждый аргумент будет либо PyObject*PyObject обычная структура C) или какой-то базовый тип C, например const char*, int, float, Поэтому я предполагаю, что облегченная реализация может быть наиболее подходящей (я не фанат чрезмерного обобщения). Но я действительно заинтересован в приобретении правильного мышления для решения таких проблем, как эти.

5

Решение

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

Прежде всего, это шаблон функции:

struct A
{
template <typename... Args>
void f(Args... args)
{
}
};

И это не шаблон функции:

template <typename... Args>
struct A
{
void f(Args... args)
{
}
};

В первом определении (с шаблоном функции) происходит вывод типа аргумента. В последнем случае нет вывода типа.

Вы не используете шаблон функции. Вы используете не шаблонную функцию-член из шаблона класса, и для этой конкретной функции-члена ее сигнатура фиксирована.

Определяя ваши trap класс, как показано ниже:

template <typename T, T t>
struct trap;

template <typename R, typename... Args, R(Base::*t)(Args...)>
struct trap<R(Base::*)(Args...), t>
{
static R call(Args... args);
};

и ссылаясь на его функцию-член, как показано ниже:

&trap<decltype(&Base::target), &Base::target>::call;

вы получите указатель на статический не шаблон call функция с фиксированной подписью, идентичная подписи target функция.

Теперь, когда call Функция служит промежуточным инвокером. Вы будете звонить call функция, и эта функция будет вызывать target функция-член, передавая свои собственные аргументы для инициализации targetПараметры, скажем:

template <typename R, typename... Args, R(Base::*t)(Args...)>
struct trap<R(Base::*)(Args...), t>
{
static R call(Args... args)
{
return (get_base()->*t)(args...);
}
};

Предположим, target функция, используемая для создания экземпляра trap Шаблон класса определяется следующим образом:

struct Base
{
int target(Noisy& a, Noisy b);
};

Создавая trap класс вы в конечном итоге со следующим call функция:

// what the compiler *sees*
static int call(Noisy& a, Noisy b)
{
return get_base()->target(a, b);
}

К счастью, a передается по ссылке, он просто перенаправлен и связан тем же типом ссылки в targetпараметр. К сожалению, это не относится к b объект — неважно, если Noisy класс является подвижным или нет, вы делаете несколько копий b экземпляр, так как этот передается по значению:

  • первый: когда call Функция вызывается сама из внешнего контекста.

  • второй: скопировать b экземпляр при вызове target функция от тела call,

ДЕМО 1

Это несколько неэффективно: вы могли бы сохранить хотя бы один вызов конструктора копирования, превратив его в вызов конструктора перемещения, если бы только вы могли включить b экземпляр в xvalue:

static int call(Noisy& a, Noisy b)
{
return get_base()->target(a, std::move(b));
//                           ~~~~~~~~~~~^
}

Теперь он будет вызывать конструктор перемещения вместо второго параметра.

Пока все хорошо, но это было сделано вручную (std::move добавил, зная, что можно применить семантику перемещения). Теперь возникает вопрос, как можно применить ту же функциональность при работе на пакет параметров?:

return get_base()->target(std::move(args)...); // WRONG!

Вы не можете подать заявку std::move вызов каждого аргумента в пакете параметров. Это, вероятно, приведет к ошибкам компилятора, если применять их одинаково ко всем аргументам.

ДЕМО 2

К счастью, хотя Args... это не пересылка ссылок, std::forward вместо этого можно использовать вспомогательную функцию. То есть в зависимости от того, что <T> тип находится в std::forward<T> (ссылка lvalue или ссылка не lvalue) std::forward будет вести себя по-другому:

  • для ссылок lvalue (например, если T является Noisy&): категория значений выражения остается lvalue (т.е. Noisy&).

  • для не-lvalue-ссылок (например, если T является Noisy&& или равнина Noisy): категория значений выражения становится значением xvalue (т.е. Noisy&&).

Сказав это, определив target функция как ниже:

static R call(Args... args)
{
return (get_base()->*t)(std::forward<Args>(args)...);
}

вы в конечном итоге:

static int call(Noisy& a, Noisy b)
{
// what the compiler *sees*
return get_base()->target(std::forward<Noisy&>(a), std::forward<Noisy>(b));
}

превращение значения в категорию выражения, включающего b в значение b, который Noisy&&, Это позволяет компилятору выбрать конструктор перемещения для инициализации второго параметра target функция, оставляя a неповрежденными.

ДЕМО 3 (сравните вывод с DEMO 1)

По сути, это то, что std::forward для. Обычно, std::forward используется с пересылка ссылок, где T содержит тип, выведенный в соответствии с правилами вывода типа для пересылки ссылок. Обратите внимание, что от вас всегда требуется проходить через <T> явно, так как он будет применять другое поведение в зависимости от этого типа (не в зависимости от категории значения своего аргумента). Без явного аргумента шаблона типа <T>, std::forward всегда выводит ссылки lvalue для аргументов, на которые ссылаются через их имена (например, при расширении пакета параметров).

Теперь вы хотели дополнительно преобразовать некоторые аргументы из одного типа в другой, переадресовывая все остальные. Если вас не волнует трюк с std::forwardПринимая аргументы из пакета параметров, можно всегда вызывать конструктор копирования, а затем вашу версию. в порядке:

template <typename T>           // transparent function
T&& process(T&& t) {
return std::forward<T>(t);
}

Bar process(Foo x) {            // overload for specific type of arguments
return Bar{x};
}

//...
get_base()->target(process(args)...);

ДЕМО 4

Однако, если вы хотите избежать копирования этого Noisy аргумент в демке, нужно как-то совместить std::forward позвонить с process вызов, а также пройти через Args типы, так что std::forward может применить правильное поведение (превращение в xvalues ​​или ничего не делать). Я просто дал вам простой пример того, как это можно реализовать:

template <typename T, typename U>
T&& process(U&& u) {
return std::forward<T>(std::forward<U>(u));
}

template <typename T>
Bar process(Foo x) {
return Bar{x};
}

//...
get_base()->target(process<Args>(args)...);

Но это только один из вариантов. Его можно упростить, переписать или переупорядочить, чтобы std::forward называется, прежде чем позвонить process функция (ваша версия):

get_base()->target(process(std::forward<Args>(args))...);

ДЕМО 5 (сравните вывод с DEMO 4)

И это будет хорошо работать (то есть, с вашей версией). Итак, дело в том, что дополнительный std::forward просто немного оптимизировать ваш код, и предоставленная идея была лишь одной из возможных реализаций этой функциональности (как вы можете видеть, она дает тот же эффект).

2

Другие решения

Не будет ли достаточно первой части версии 2? только:

template <typename T, typename U>
T&& process(U&& u) {
return std::forward<T>(std::forward<U>(u));
}

Учитывая пример использования с существующим преобразованием (конструктор для «Bar» из «Foo»), например:

struct Foo {
int x;
};
struct Bar {
int y;
Bar(Foo f) {
y = f.x;
}
};
int main() {

auto b = process<Bar>(Foo()); // b will become a "Bar"auto i = process<int>(1.5f);
}

Вы все равно должны указать первый параметр шаблона (тип для преобразования), потому что компилятор не может его определить. Таким образом, он знает, какого типа вы ожидаете, и создаст временный объект типа «Bar», потому что есть конструктор.

0

По вопросам рекламы [email protected]