Использование свойств типа С++ 11 для обеспечения альтернативных встроенных реализаций
Является ли следующий шаблон кода разумным при использовании признаков в шаблоном коде, где обе альтернативные реализации всегда компилируются?
Чтение кода кажется более ясным, чем использование других shenanigans для условной компиляции (но тогда, возможно, я просто недостаточно хорошо знаком с этими shenanigans).
template<typename T>
class X
{
void do_something() noexcept(std::is_nothrow_copy_constructible<T>::value)
{
if (std::is_nothrow_copy_constructible<T>::value)
{
// some short code that assumes T copy constructor won't throw
}
else
{
// some longer code with try/catch blocks and more complexity
}
}
// many other methods
};
(Добавленная сложность частично обеспечивает сильную гарантию исключения.)
Я знаю, что этот код будет работать, но разумно ли ожидать, что компилятор устранит константно-ложные ветки и сделает inlining и т.д. для случая без исключения (где намного проще, чем в другом случае)? Я надеюсь на что-то, что было бы столь же эффективно в noexcept случае, как писать метод только с первым блоком как тело (и наоборот, хотя я меньше беспокоюсь о сложном случае).
Если это не правильный способ сделать это, может кто-нибудь, пожалуйста, просветить меня до рекомендуемого синтаксиса?
Ответы
Ответ 1
[...] разумно ли ожидать, что компилятор устранит константно-ложные ветки и сделает вложение и т.д. для случая без исключения (где намного проще, чем в другом случае)?
Это может быть, но я бы не стал полагаться на это, потому что вы не можете его контролировать.
Если вы хотите удалить if/else
, вы можете скрыть тип возвращаемого значения и очистить квалификатор noexcept
.
В качестве примера:
template<typename T>
class X {
template<typename U = T>
std::enable_if_t<std::is_nothrow_copy_constructible<T>::value>
do_something()
noexcept(true)
{}
template<typename U = T>
std::enable_if_t<not std::is_nothrow_copy_constructible<T>::value>
do_something()
noexcept(false)
{}
};
Недостатки в том, что теперь у вас есть два шаблона функций-членов.
Не уверен, что он соответствует вашим требованиям.
Если вам разрешено использовать функции из С++ 17, if constexpr
- это, вероятно, путь, и вам больше не нужно прерывать свой метод в двух функциях-членах.
Другой подход может быть основан на теге-диспетчеризации noexcept
вашего типа.
В качестве примера:
template<typename T>
class X {
void do_something(std::true_type)
noexcept(true)
{}
void do_something(std::false_type)
noexcept(false)
{}
void do_something()
noexcept(do_something(std::is_nothrow_copy_constructible<T>{}))
{ do_something(std::is_nothrow_copy_constructible<T>{}); }
};
Я знаю, sfinae - это не глагол, а сфайна, что-то звучит так хорошо.
Ответ 2
разумно ли ожидать, что компилятор устранит константно-ложные ветки и сделает вложение и т.д. для случая без исключения [...]?
Да. При этом необходимо создать экземпляр константно-ложной ветки, что может или не может заставить компилятор создать экземпляр кучки символов, которые вам не нужны (и тогда вам нужно полагаться на компоновщик, чтобы удалить их).
Я все равно поеду с shenanigans SFINAE (на самом деле тег-диспетчеризация), что можно сделать очень легко в С++ 11.
template<typename T>
class X
{
void do_something() noexcept(std::is_nothrow_copy_constructible<T>::value)
{
do_something_impl(std::is_nothrow_copy_constructible<T>() );
}
void do_something_impl( std::true_type /* nothrow_copy_constructible */ )
{
// some short code that assumes T copy constructor won't throw
}
void do_something_impl( std::false_type /* nothrow_copy_constructible */)
{
// some longer code with try/catch blocks and more complexity
}
// many other methods
};
Если вы хотите проверить nothrow_copy_constructor
во всех других методах, вы можете рассмотреть специализацию всего класса:
template<typename T, class = std::is_nothrow_copy_constructible_t<T> >
class X
{
//throw copy-ctor implementation
};
template<typename T>
class X<T, std::true_type>
{
// noexcept copy-ctor implementation
};
Ответ 3
Можно ли ожидать, что компилятор устранит константно-ложные ветки?
Да, исключение мертвого кода - одна из простейших оптимизаций.
... и сделать вложение и т.д. для случая без исключения?
Мой первый импульс состоял в том, чтобы ответить "Нет, вы не можете полагаться на это, так как это зависит от того, где проходит проход вставки в потоке оптимизации относительно этапа устранения мертвого кода".
Но при большом отражении я не понимаю, почему зрелый компилятор на достаточно высоком уровне оптимизации не будет выполнять удаление мертвого кода как до, так и после этапа вложения. Поэтому это ожидание должно быть разумным.
Однако угадывание в отношении оптимизаций никогда не является верным. Пойдите для простой реализации и получите корректно функционирующий код. Затем измерьте его производительность и проверьте, были ли ваши предположения истинными. Если бы они не были реинжинирингом, реализация для вашей ситуации не займет значительно больше времени, чем если бы вы спустили гарантированный путь с самого начала.
Ответ 4
Каждый зрелый компилятор уничтожает код. Каждый зрелый компилятор обнаруживает постоянные ветки, а мертвая кодирует другую ветвь.
Вы можете создать функцию с дюжиной аргументов шаблона, которая использует наивные if
проверки в своем теле и посмотреть на полученный результат - проблема не будет.
Если вы делаете такие вещи, как создавать переменные static
или thread_local
или создавать экземпляры символов, их все труднее устранить.
Вложения немного сложнее, потому что компиляторы в какой-то момент отказываются от вложения; чем сложнее код, тем вероятнее, что компилятор откажется, прежде чем вставлять его.
В С++ 17 вы можете обновить версию if
до версии constexpr
. Но в С++ 14 и 11 ваш код будет отлично. Это проще и легче читать, чем альтернативы.
Он несколько хрупкий, но если он ломается, он обычно делает это во время компиляции с шумом.
Ответ 5
но разумно ли ожидать, что компилятор устранит ветки с постоянной ложью
Нет. Все ветки будут оцениваться компилятором. Вы можете попробовать использовать if constexpr
из С++ 17.
То, что вы пытаетесь достичь, это SFINAE.
Ответ 6
Вы можете попытаться реализовать constexpr_if
самостоятельно. Решение С++ 11 может выглядеть следующим образом:
#include <iostream>
#include <type_traits>
template <bool V>
struct constexpr_if {
template <class Lambda, class... Args>
static int then(Lambda lambda, Args... args) {
return 0;
}
};
template <>
struct constexpr_if<true> {
template <class Lambda, class... Args>
static auto then(Lambda lambda, Args... args) -> decltype(lambda(args...)) {
return lambda(args...);
}
static int then(...) {
return 0;
}
};
struct B {
B() {}
B(const B &) noexcept {}
void do_something() {
std::cout << "B::do_something()" << std::endl;
}
};
struct C {
C() {}
C(const C &) noexcept {}
void do_something_else() {
std::cout << "C::do_something_else()" << std::endl;
}
};
struct D {
D() {}
D(const D &) throw(int) {}
void do_something_else() {
std::cout << "D::do_something_else()" << std::endl;
}
};
template <class T>
struct X {
void do_something() {
T t;
constexpr_if<std::is_nothrow_copy_constructible<T>::value>::then([](B &b) {
b.do_something();
}, t);
constexpr_if<std::is_nothrow_copy_constructible<T>::value>::then([](C &c) {
c.do_something_else();
}, t);
constexpr_if<!std::is_nothrow_copy_constructible<T>::value>::then([](D &d) {
d.do_something_else();
}, t);
}
};
int main() {
X<B> x;
x.do_something();
X<C> xx;
xx.do_something();
X<D> xxx;
xxx.do_something();
}
Вывод:
B::do_something()
C::do_something_else()
D::do_something_else()