Задача состоит в том, чтобы создать функцию с одним аргументом, которая перенаправляет все типы, кроме одного (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
, Поэтому я предполагаю, что облегченная реализация может быть наиболее подходящей (я не фанат чрезмерного обобщения). Но я действительно заинтересован в приобретении правильного мышления для решения таких проблем, как эти.
Я чувствую незначительное заблуждение в вашем понимании варианта использования, с которым вы сталкиваетесь.
Прежде всего, это шаблон функции:
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
,
Это несколько неэффективно: вы могли бы сохранить хотя бы один вызов конструктора копирования, превратив его в вызов конструктора перемещения, если бы только вы могли включить 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
вызов каждого аргумента в пакете параметров. Это, вероятно, приведет к ошибкам компилятора, если применять их одинаково ко всем аргументам.
К счастью, хотя 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)...);
Однако, если вы хотите избежать копирования этого 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? только:
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», потому что есть конструктор.