Использование признаков типа C++11 для обеспечения альтернативных встроенных реализаций


Является ли следующий шаблон кода разумным при использовании признаков в шаблонном коде, где обе альтернативные реализации всегда компилируемы?

Чтение кода кажется более ясным, чем выполнение других трюков для условной компиляции (но тогда, возможно, я просто недостаточно знаком с этими трюками).

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's copy constructor won't throw
        }
        else
        {
            // some longer code with try/catch blocks and more complexity
        }
    }

    // many other methods
};

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

Я знаю, что этот код будет работать , но разумно ли ожидать, что компилятор, чтобы исключить константы-ложные ветви и сделать inlining etc для случая noexcept (где гораздо проще, чем в другом случае)? Я надеюсь на что-то, что будет столь же эффективным в случае noexcept, как написание метода только с этим первым блоком в качестве тела (и наоборот, хотя я меньше беспокоюсь о сложном случае).

Если это не правильный способ, может кто-нибудь просветить меня относительно рекомендуемого синтаксиса?

6 28

6 ответов:

[...] разумно ли ожидать, что компилятор устранит ветви constant-false и сделает вставку etc для случая noexcept (где намного проще, чем в другом случае)?

Это может быть, но я бы не стал полагаться на это, потому что вы не можете контролировать это.

Если вы хотите удалить if/else, Вы можете sfinae возвращаемый тип и очистить квалификатор 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)
    {}
};

Недостатки заключаются в том, что теперь у вас есть две функции-члена шаблон.
Не уверен, что он соответствует вашим требованиям.

Если вам разрешено использовать функции из C++17, if constexpr, вероятно, это правильный путь, и вам больше не нужно разбивать свой метод на две функции-члена.

Другой подход может быть основан надиспетчеризации тегов noexceptness вашего типа.
В качестве примера:

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 не глагол, но для sfinae что-то звучит так хорошо.

Разумно ли ожидать, что компилятор устранит ветви constant-false и сделает inlining etc для случая noexcept [...]?

Да. Тем не менее, ветвь constant-false должна быть создана, что может привести или не привести к тому, что компилятор создаст набор символов, которые вам не нужны (а затем вам нужно будет полагаться на компоновщик, чтобы удалить их).

Я бы все равно пошел с SFINAE shenanigans (фактически tag-dispatching), что можно сделать очень легко в C++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's 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
};

Разумно ли ожидать, что компилятор устранит ветви constant-false?

Да, устранение мертвого кода - одна из простейших оптимизаций.

... и сделать подстановку и т. д. В случае, как noexcept?

Моим первым побуждением было ответить:"Нет, на это нельзя полагаться, так как это зависит от того, где в потоке оптимизации находится входящий проход относительно шага устранения мертвого кода".

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

Однако угадывание в отношении оптимизаций никогда не бывает верным. Перейдите к простой реализации и получите правильно функционирующий код. Затем измерьте его эффективность и проверьте, были ли ваши предположения верны. Если бы их не было - перепроектирование реализации для вашей ситуации не займет много времени. значительно больше времени, чем если бы вы шли по гарантированному пути с самого начала.

Каждый зрелый компилятор выполняет удаление мертвого кода. Каждый зрелый компилятор обнаруживает постоянные ветви и мертвые коды другой ветви.

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

Если вы делаете такие вещи, как создание переменных static или thread_local или создание экземпляров символов, их все труднее устранить.

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

В C++17 вы можете обновить свой if до версии constexpr. Но в C++14 и 11 ваш код будет работать просто отлично. Это проще и легче читать, чем альтернативы.

Он несколько хрупок, но если он ломается, то обычно делает это во время компиляции шумным способом.

Но разумно ли ожидать, что компилятор устранит ветви constant-false

Нет. Все ветви будут оцениваться компилятором. Вы можете попробовать использовать if constexpr из c++17.

То, чего вы пытаетесь достичь, - это SFINAE.

Вы можете попробовать реализовать constexpr_if самостоятельно. решение c++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()