#c #c 11 #perfect-forwarding #universal-reference #forwarding-reference
#c #c 11 #идеальная пересылка #универсальный-ссылка #пересылка-ссылка
Вопрос:
До c 11 я писал код, подобный этому:
// Small functions
void doThingsWithA(const Aamp; a)
{
// do stuff
}
void doThingsWithB(const Bamp; b)
{
// do stuff
}
void doThingsWithC(const Camp; c)
{
// do stuff
}
// Big function
void doThingsWithABC(const Aamp; a, const Bamp; b, const Camp; c)
{
// do stuff
doThingsWithA(a);
doThingsWithB(b);
doThingsWithC(c);
// do stuff
}
Но теперь, с семантикой перемещения, может стать интересным (по крайней мере, в некоторых случаях) разрешить моим функциям принимать ссылки rvalue в качестве параметров и добавлять эти перегрузки:
void doThingsWithA(Aamp;amp; a);
void doThingsWithB(Bamp;amp; b);
void doThingsWithC(Camp;amp; c);
Из того, что я понял, если я хочу иметь возможность вызывать эти перегрузки в моей большой функции, мне нужно использовать perfect forwarding , который может выглядеть следующим образом (это немного менее читабельно, но я думаю, что это может быть нормально с хорошим соглашением об именовании для типов шаблонов):
template<typename TplA, typename TplB, typename TplC>
void doThingsWithABC(TplAamp;amp; a, TplBamp;amp; b, TplCamp;amp; c)
{
// do stuff
doThingsWithA(std::forward<TplA>(a));
doThingsWithB(std::forward<TplB>(b));
doThingsWithC(std::forward<TplC>(c));
// do stuff
}
Моя проблема заключается в следующем: не означает ли это, что если мои небольшие функции будут иметь другие перегрузки, станет возможным вызвать большую с параметрами типов, для которых она не предназначалась?
Я думаю, что это может помочь предотвратить это:
template<typename TplA, typename TplB, typename TplC,
class = typename std::enable_if<std::is_same<A, std::decay<TplA>::type>::value>::type,
class = typename std::enable_if<std::is_same<B, std::decay<TplB>::type>::value>::type,
class = typename std::enable_if<std::is_same<C, std::decay<TplC>::type>::value>::type>
doThingsWithABC(TplAamp;amp; a, TplBamp;amp; b, TplCamp;amp; c)
{
// do stuff
doThingsWithA(std::forward<TplA>(a));
doThingsWithB(std::forward<TplB>(b));
doThingsWithC(std::forward<TplC>(c));
// do stuff
}
Хотя я не уверен, что это не слишком ограничительно, поскольку я понятия не имею, как это будет себя вести, если я попытаюсь вызвать большие функции с типами, которые неявно преобразуются в A, B или C…
Но … даже если предположить, что это работает, у меня действительно нет других вариантов? (Я имею в виду … это нелегко для глаз)
Комментарии:
1. Вы можете использовать
static_assert
, если вам так проще.2. Вы могли бы использовать макрос для создания всех 8 версий
doThingsWithABC
… утки
Ответ №1:
Идеальная пересылка в основном предназначена для случаев, когда вы не знаете, как будут использоваться данные, потому что вы пишете универсальную оболочку «предоставленных пользователем» данных.
В простой процедурной системе, подобной описанной выше, 3 вещи, которые вы делаете, будут конкретными задачами.
Это означает, что вы будете знать, выиграют ли они от наличия подвижного источника данных или нет, и имеют ли они смысл, если их нужно копировать, и дешево ли перемещать.
Если копирование имеет смысл, но перемещение выполняется быстрее, а перемещение обходится дешево (обычный случай), они должны принимать параметры по значению и удалять их при сохранении своей локальной копии.
Затем это правило рекурсивно применяется к функции, которая вызывает 3 подфункции.
Если функция не выигрывает от перемещения, используйте by constamp;
.
Если копирование не имеет смысла, возьмите по ссылке rvalue (не универсальной ссылке) или по значению.
В случае, когда иметь возможность move
и move
хорошо, и дорого, вы должны рассмотреть возможность идеальной пересылки. Как отмечалось выше, это обычно происходит только при переносе функций, установленных «пользователем» вашей базы кода, что обычно move
либо действительно дешево, либо так дорого, как копирование. Вы должны быть на промежуточной или неопределенной стадии move
эффективности, чтобы идеальная пересылка стоила того.
Существуют и другие способы идеальной пересылки, такие как мутаторы контейнеров, но они более эзотеричны. В качестве примера, мой backwards
range mutator идеально перенаправит входящий диапазон в хранилище, чтобы расширение срока службы ссылки работало должным образом, когда вы объединяете несколько range mutators в циклах на основе диапазона в стиле C 11 for(:)
.
Безумно совершенная пересылка приводит к раздуванию сгенерированного кода, медленным сборкам, непрочным реализациям и трудному для понимания коду.
Ответ №2:
Используйте static_assert
s вместо enable_if
. ИМХО, этот вариант не только проще для глаз, но и более удобен для пользователя. Компилятор выведет четкое сообщение об ошибке, если типы аргументов нарушены, тогда как в случае с enable_if
аналогом он будет жаловаться на то, что соответствующая функция не найдена.
template<typename TplA, typename TplB, typename TplC>
void doThingsWithABC(TplAamp;amp; a, TplBamp;amp; b, TplCamp;amp; c)
{
static_assert(std::is_same<A, std::decay<TplA>::type>::value, "arg1 must be of type A");
static_assert(std::is_same<B, std::decay<TplB>::type>::value, "arg2 must be of type B");
static_assert(std::is_same<C, std::decay<TplC>::type>::value, "arg3 must be of type C");
// do stuff
doThingsWithA(std::forward<TplA>(a));
doThingsWithB(std::forward<TplB>(b));
doThingsWithC(std::forward<TplC>(c));
// do stuff
}
Комментарии:
1.
static_assert
действительно, более читабелен. Однако это не будет работать, когда есть другие перегрузки функций. В этом случаеenable_if
(SFINAE) является единственным вариантом.