Концепты для отчаявшихся

    Всё началось с того, что мне понадобилось написать функцию, принимающую на себя владение произвольным объектом. Казалось бы, что может быть проще:


    template <typename T>
    void f (T t)
    {
        // Завладели экземпляром `t` типа `T`.
        ...
    
        // Хочешь — переноси.
        g(std::move(t));
    
        // Не хочешь — не переноси.
        ...
    }

    Но есть один нюанс: требуется, чтобы принимаемый объект был строго rvalue. Следовательно, нужно:


    1. Сообщать об ошибке компиляции при попытке передать lvalue.
    2. Избежать лишнего вызова конструктора при создании объекта на стеке.

    А вот это уже сложнее сделать.


    Поясню.


    Требования к входным аргументам


    Допустим, мы хотим обратного, то есть чтобы функция принимала только lvalue и не компилировалась, если ей на вход подаётся rvalue. Для этого в языке присутствует специальный синтаксис:


    template <typename T>
    void f (T & t);

    Такая запись означает, что функция f принимает lvalue-ссылку на объект типа T. При этом заранее не оговариваются cv-квалификаторы. Это может быть и ссылка на константу, и ссылка на неконстанту, и любые другие варианты.


    Но ссылкой на rvalue она быть не может: если передать в функцию f ссылку на rvalue, то программа не скомпилируется:


    template <typename T>
    void f (T &) {}
    
    int main ()
    {
        auto x = 1;
        f(x); // Всё хорошо, T = int.
    
        const auto y = 2;
        f(y); // Всё хорошо, T = const int.
    
        f(6.1); // Ошибка компиляции.
    }

    Может, есть синтаксис и для обратного случая, когда нужно принимать только rvalue и сообщать об ошибке при передаче lvalue?


    К сожалению, нет.


    Единственная возможность принять rvalue-ссылку на произвольный объект — это сквозная ссылка (forwarding reference):


    template <typename T>
    void f (T && t);

    Но сквозная ссылка может быть ссылкой как на rvalue, так и на lvalue. Следовательно, нужного эффекта мы пока не добились.


    Добиться нужного эффекта можно при помощи механизма SFINAE, но он достаточно громоздкий и неудобный как для написания, так и для чтения:


    #include <type_traits>
    
    template <typename T,
        typename = std::enable_if_t<std::is_rvalue_reference<T &&>::value>>
    void f (T &&) {}
    
    int main ()
    {
        auto x = 1;
        f(x); // Ошибка компиляции.
    
        f(std::move(x)); // Всё хорошо.
    
        f(6.1); // Всё хорошо.
    }

    А чего бы на самом деле хотелось?


    Хотелось бы вот такой записи:


    template <typename T>
    void f (rvalue<T> t);

    Думаю, смысл данной записи выражен достаточно чётко: принять произвольное rvalue.


    Первая мысль, которая приходит в голову, — это создать псевдоним типа:


    template <typename T>
    using rvalue = T &&;

    Но такая штука, к несчастью, не сработает, потому что подстановка псевдонима происходит до вывода типа шаблона, поэтому в данной ситуации запись rvalue<T> в аргументах функции полностью эквивалентна записи T &&.


    Скрытый текст

    Забавно, что из-за ошибки в системе вывода типов компилятора Clang (версию точно не помню, кажется, 3.6) этот вариант "сработал". В компиляторе GCC этой ошибки не было, поэтому поначалу мой затуманенный безумной идеей разум решил, что ошибка не в Кланге, а в Гэцэцэ. Но, проведя, небольшое расследование, я понял, что это не так. А через некоторое время и в Кланге эту ошибку исправили.


    Ещё одна идея — по сути, аналогичная, — которая может прийти в голову знатоку шаблонного метапрограммирования — это написать следующий код:


    template <typename T>
    struct rvalue_t
    {
        using type = T &&;
    };
    
    template <typename T>
    using rvalue = typename rvalue_t<T>::type;

    К структуре rvalue_t можно было бы припилить SFINAE, которое отваливалось бы, если бы T было ссылкой на lvalue.


    Но, к сожалению, эта идея также обречена на провал, потому что такая структура "ломает" механизм вывода типов. В результате функцию f вообще будет невозможно вызвать без явного указания аргумента шаблона.


    Я очень расстроился и на время забросил эту идею.


    Возвращение


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


    Немного поразмыслив, я сформулировал "требования":


    1. Должен работать механизм вывода типа.
    2. Должна быть возможность натравливать SFINAE-проверки на выводимый тип.

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


    Тогда возникает закономерный вопрос: можно ли натравливать SFINAE на псевдонимы типов?


    Оказывается, можно. И выглядеть это будет, например, следующим образом:


    template <typename T,
        typename = std::enable_if_t<std::is_rvalue_reference<T &&>::value>>
    using rvalue = T &&;

    Наконец-то получаем и требуемый интерфейс, и требуемое поведение:


    template <typename T>
    void f (rvalue<T>) {}
    
    int main ()
    {
        auto x = 1;
        f(x); // Ошибка компиляции.
    
        f(std::move(x)); // Всё хорошо.
    
        f(6.1); // Всё хорошо.
    }

    Победа.


    Концепты


    Внимательный читатель негодует: "Так где же тут концепты-то?".


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


    template <typename I,
        typename = std::enable_if_t<std::is_integral<I>::value>>
    using Integer = I;
    
    template <typename I>
    void g (Integer<I> t);

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


    int main ()
    {
        g(1); // Всё хорошо.
        g(1.2); // Ошибка компиляции.
    }

    Что ещё можно сделать?


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


    template <Integer I>
    void g (I n);

    Для этого воспользуемся, кхм, макроснёй:


    #define Integer(I) typename I, typename = Integer<I>

    Получим возможность писать следующий код:


    template <Integer(I)>
    void g (I n);

    На этом возможности данной техники, пожалуй, заканчиваются.


    Недостатки


    Если вспомнить название статьи, то можно подумать, что у этой техники есть какие-то недостатки.


    Таки да. Есть.


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


    template <typename I>
    void g (Integer<I>) {}
    
    template <typename I>
    void g (Floating<I>) {}

    и будет выдавать ошибку о переопределении функции g.


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


    Выводы


    Приведённая техника — назовём её техникой фильтрующего псевдонима типов — имеет достаточно ограниченную область применения.


    Но в тех случаях, когда она применима, она открывает программисту достаточно неплохие возможности для чёткого выражения намерения в коде.


    Считаю, что она имеет право на жизнь. Лично я пользуюсь. И не жалею.


    Ссылки по теме


    1. Библиотека "Boost Concept Check"
    2. Концепты из прототипа библиотеки диапазонов "range-v3"
    3. Библиотека "TICK"
    4. Статья "Concepts Without Concepts"
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 53

      +4
      Зачем нужен запрет передавать lvalue? Как архитектурное решение — кажется неудачным.
        +4
        Зачем нужен запрет передавать lvalue?

        Подчеркнуть, что у входного объекта нет других владельцев.


        Как архитектурное решение — кажется неудачным.

        Почему?

        +2
        Божественно. Особенно радует, что это не экран шаблонного кода, а вполне вменяемые 2 строки. Удачно отделяется концепт с условием от определения самого метода. Всё таки даже без концептов С++11 силён.
        Долго соображал над конструкцией typename = std::enable_if_t<...>. Я правильно понял, что это просто безымянный аргумент шаблона для using? А то на первый взгляд выглядит как специальный синтаксис.
          +3
          да, и с паттерн матчингом который отключит компиляцию если тип не std::is_integral
            +2
            Я правильно понял, что это просто безымянный аргумент шаблона для using?

            Совершенно верно.
            Это безымянный аргумент шаблона со значением по-умолчанию.

              0
              Какой смысл в безымянных шаблонных параметрах? Зачем это вообще компилируется, почему не синтаксическая ошибка?
                +4

                Какой смысл в безымянных аргументах функции? Зачем это вообще компилируется, почему не синтаксическая ошибка?

                  +1

                  Встроенная в мозг дифалка не сразу уловила разницу, нужно было хотя бы жирным выделить "аргументах функции" :)

                  +5
                  Смысл безымянного параметра в названии — нам не интересно его имя, мы его далее никак не используем. А сам параметр нужен для SFINAE
              +5
              Использовать перегрузку функций по концептам можно с помощью следующего способа:

              template <typename T>
              auto foo(T i) -> enable_if_t<is_integral_v<T>, void> {
                  printf("Int\n");
              }
              
              template <typename T>
              auto foo(T i) -> enable_if_t<is_floating_point_v<T>, void> {
                  printf("Float\n");
              }
              
              template <typename T>
              auto foo(T i) -> enable_if_t<is_class_v<T>, void> {
                  printf("Struct\n");
              }
              
              int main() {
                  foo(1);
                  foo(1u);
                  foo(1.2);
                  foo(1.2f);
                  foo(make_tuple(1));
                  return 0;
              }
              


              Или так:

              template <typename T, typename = enable_if_t<is_integral_v<T>>>
              using IsInteger = integral_constant<int, 0>;
              
              template <typename T, typename = enable_if_t<is_floating_point_v<T>>>
              using IsFloat = integral_constant<int, 1>;
              
              template <typename T, typename = enable_if_t<is_class_v<T>>>
              using IsStruct = integral_constant<int, 2>;
              
              template <typename T>
              auto foo(T i, IsInteger<T> = {}) -> void {
                  printf("Int\n");
              }
              
              template <typename T>
              auto foo(T i, IsFloat<T> = {}) -> void {
                  printf("Float\n");
              }
              
              template <typename T>
              auto foo(T i, IsStruct<T> = {}) -> void {
                  printf("Struct\n");
              }
              


              Не так элегантно, но тем не менее. Будем надеяться, что концепты всё таки включат в стандарт когда-нибудь.
                0

                Если хочется более полноценной работы с концептами, то нужно смотреть в сторону имеющихся библиотек (см. ссылки в конце статьи).


                Смысл приведённой техники именно в простоте и удобочитаемости.

                  +2
                  Лучше не использовать продвинутые заменители концептов до их появления в стандарте.

                  Причины простые:
                  1. Крайне неинформативный вывод сообщений об ошибках в шаблонах.
                  2. Замедление скорости компиляции.
                  3. Не всегда очевидна логика работы шаблонных конструкций без вдумчивого анализа кода.
                  4. В конце концов, монструозные конструкции ломают IDE, в результате чего IDE превращается просто в редактор с подсветкой синтаксиса.

                  Указанный в публикации пример — исключение, подтверждающее правило.

                  P.S. Я бы просто воткнул в функцию static_assert.
                    0
                    +1, static_assert будет наиболее предпочтителен, если не нужно использовать перегрузку.
                      +2
                      Замедление скорости компиляции.

                      не думаю, что концепты сильно её ускорят, скорее наоборот. Ну т.е. код с одинаковым набором ограничений при помощи концептов и SFINAE думаю будет компилироваться со сравнительной скоростью. Нужно попробовать сравнить: в gcc-6 концепты завезли.

                  +1
                  Синтаксис настолько близок к настоящим концептам, что можно попробовать сделать универсальные макросы, которые будут использовать эту технику при отсутствии поддержки концептов в компиляторе. Если это удастся, то можно будет писать такой код, который будет компилиться как на нормальном компиляторе, так и на «concept-enabled» gcc.
                    0

                    Поздравляю, вы переизобрели кусочек Concepts Lite! Которые, как вы упомянули, надысь выбросили из C++17. Вы можете сравнить свою логику и логику Саттона (Andrew Sutton), если включите какой-нибудь его доклад на Ютубе. Идея та же: CL это лёгкая обёртка над enable_if. Проблем несколько. Первая порция, которая касается вашего решения и не касается его реализации на уровне компилятора: это, как тоже сказали выше, время компиляции и, более важно, информативность ошибок (особенно в вашей версии с макросом она будет жуткой, думаю). Саттон специально отмечает, что на уровне компилятора это лучше решать.


                    Про перегрузку вы сами написали. Есть ещё проблема, которая касается и вашего решения, и CL: нет проверки ограничений внутри тела шаблона. То есть вы можете выставить какой-то концепт-интерфейс, но внутри шаблона использовать больше, чем затребовали в этом интерфейсе (по недосмотру, который не так уж нереален в таком тяжёлом и синтаксически и семантически языке как C++). Конечно, ошибка будет на стадии компиляции. Но всё равно досадно. Особенно если это библиотека и на неё напорется какой-то неискушённый пользователь этой библиотеки.

                      +2

                      Я не понял вашей мысли.


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


                      Я просто сымитировл часть функциональности, которая (и я об этом открыто говорю в статье) не даёт никаких преимуществ над другими методами пред-C++17, за исключением удобочитаемости.

                        0

                        Я ни в чём никого не упрекаю. Моя мысль такая, что прослушав пару докладов Саттона одно или двухгодичной давности можно было бы написать вариант с определением rvalue через enable_if+type_traits довольно быстро. Потому что там это всё разжёвывается хорошо. Ну и компилятор писать я вас ни в коем случае не призываю. Просто пишу о пользе просмотра докладов ведущих специалистов.

                          0

                          Можно ссылки на доклады, о которых идёт речь?

                              0

                              Я посмотрел эти доклады.


                              В них прекрасно рассказывается об "упрощённых концептах", но о технике, про которую речь в моей публикации, там ничего не говорится.

                      +5
                      А почему не объявить вот так?
                      void g(T &t) = delete;
                      void g(T &&t);
                      

                      Тогда lvalue замапится на первый вариант и выдаст ошибку компиляции.
                        0

                        Преимущества записи rvalue<T> лично я вижу следующие:


                        1. Она явно выражает намерение автора.


                          Удаление варианта с lvalue-ссылкой, в каком-то смысле, тоже явно выражает намерение. Но тогда читателю перегрузки со сквозной ссылкой нужно помнить, что сквозная ссылка на самом деле не может быть lvalue-ссылкой, а перегрузка с lvalue-ссылкой явно удалена.


                        2. Функция становится самодостаточной.


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


                          0
                          Согласен. Но как решение для мира, где нет концептов, должно вполне годиться.
                          0
                          с lvalue gcc говорит, что
                          error: call of overloaded 'bar(int&)' is ambiguous
                          bar(a);
                          ^

                          Потому что обе сигнатуры — одно и то же для перегрузок. То есть всё работает, но ошибка совсем не информативна.
                          P.S. Лучше воспользоваться не =delete, а static_assert (false, «lvalue is not supported»);
                            +2

                            Пора обновлять. Уже 5.4 и 6.1 вышли, а вы всё на 4.8 сидите ;) .

                              0
                              В том-то и дело, что не одно и тоже, в этом и фишка.

                              void g(int &i) = delete;
                              void g(int &&i);
                              
                              void foo(int i) {
                                g(i);
                                g(5);
                              }
                              


                              clang 3.8:
                              > clang++ -c foo.cpp -std=c++11
                              foo.cpp:5:3: error: call to deleted function 'g'
                              g(i);
                              ^
                              foo.cpp:1:6: note: candidate function has been explicitly deleted
                              void g(int &i) = delete;
                              ^
                              foo.cpp:2:6: note: candidate function not viable: no known conversion from 'int' to 'int &&' for 1st argument
                              void g(int &&i);
                              ^
                              1 error generated.

                              gcc 5.3.1:
                              > g++ -c foo.cpp -std=c++11
                              foo.cpp: In function ‘void foo(int)’:
                              foo.cpp:5:6: error: use of deleted function ‘void g(int&)’
                              g(i);
                              ^
                              foo.cpp:1:6: note: declared here
                              void g(int &i) = delete;
                              ^
                                +2
                                static_assert с первым аргументом, не зависящим от типа шаблона, имеет право компилятором вычисляться до соответствующей инстанциации шаблона. Соответственно, вы получите ошибку всегда, даже если эту функцию и не думали вызывать. Пруф.

                                Можно, впрочем, обойти костылём.
                              +1
                              Когда вы в статье упомянули о том, что нужно сообщать об ошибке компиляции, мне сразу подумалось, что проблему можно решить через static_asser, как уже предлагали выше. Например вот так:

                              static_assert(std::is_rvalue_reference <T &&>::value,"Message here");
                                +3

                                Явное лучше неявного.


                                Когда я пишу rvalue<T>, я сообщаю читателю моего кода, что у меня здесь на входе всегда rvalue.
                                Если же я пишу обычную сквозную ссылку и static_assert внутри функции, то читателю сложнее понять моё намерение. Потому что ему ещё нужно дочитать до этой проверки. А потом ему это нужно постоянно помнить, чтобы случайно не воткнуть forward вместо move.


                                Так же, как и использование стандартных алгоритмов вместо циклов: мы сообщаем читателю что мы хотели сделать, а не как мы это сделали.


                                См. также комментарий выше.

                                0
                                Интересно, спасибо
                                  0
                                  Невольно сравнивая с теми же вещами в Rust, не могу не почувствовать разницу в простоте.
                                    +4

                                    С этого места поподробнее, пожалуйста.

                                      +1

                                      А разве на расте эта задача вообще решается?


                                      Да, из-за необходимости указывать трейты для параметров дженериков получается, что некий (обязательный) аналог концептов уже есть. И с передачей владения тоже всё неплохо сделано. Но если представить, что мы хотим решить задачу так как она сформулирована в статье, то что можем сделать?

                                        0
                                        Невольно сравнивая с теми же вещами в Haskell, не могу не почувствовать разницу в возможностях. Серьёзно, в ряде важных случаев на тайпклассах отрицание не выражается (вроде «реализовать тайпкласс для всех вещей, не реализующих тайпкласс Bounded»), по крайней мере, жутчайших костылей, после которых тайпчекер сходит с ума.
                                          0
                                          реализовать тайпкласс для всех вещей, не реализующих тайпкласс Bounded

                                          А это как? Создал тип данных, импортнул ваш модуль — и инстанс тайпкласса есть, могу им воспользоваться в функции. Добавил в соседнем инстанс Bounded — и в нём инстанса вашего тайпкласса уже нет, но функцию мою, к примеру, можно звать?
                                            0
                                            Добавил в соседнем инстанс Bounded — и в нём инстанса вашего тайпкласса уже нет, но функцию мою, к примеру, можно звать?

                                            Эм, почему? У вашей функции будет же сигнатура вроде ¬(Bounded a) => a -> foobar, разве нет?

                                            Правда, лично мне это надо было в какой-то другой задаче и локально на уровне модуля. Там даже всякие OverlappingInstances и UndecidableInstances не помогали совсем. Ну, и немудрено, на самом деле.
                                              0
                                              У вашей функции будет же сигнатура вроде ¬(Bounded a) => a -> foobar, разве нет?

                                              Необязательно, может, просто MyType -> Blah.

                                              Это просто выглядит как-то императивно что ли, когда от порядка объявления зависит код. Грубо говоря, если запретить даже orphans, то всё будет достаточно строго — появился класс и вместе с ним все инстансы, появился тип — с ним все инстансы. Всё достаточно однозначно. Необходимость в orphan instances ещё можно понять, если авторы и класса, и типа, — сторонние люди, а инстанс вполне однозначен; а вот описанная ситуация, когда от добавления инстанса другой инстанс должен пропадать, создаёт устойчивое впечатление, что что-то тут не так :)
                                                0
                                                Необязательно, может, просто MyType -> Blah

                                                А какое отношение тогда MyType имеет к Bounded? Я сходу не могу придумать, как оно у вас бы так получилось.

                                                Либо там всё совсем скрыто, и от наличия инстанса в текущем модуле ничего не сломается.

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

                                                  А это как? Создал тип данных [MyType], импортнул ваш модуль — и инстанс [ваш, для всех, у кого нет инстанса Bounded] тайпкласса есть, могу им воспользоваться в функции [foo :: MyType -> Blah]. Добавил в соседнем инстанс Bounded — и в нём инстанса вашего тайпкласса уже нет, но функцию мою [foo], к примеру, можно звать?

                                                  А если я выставлю набор функций, каждая из которых просто напросто дублирует функции вашего класса, но специализирована для MyType и соответственно реализована через позыв функций вашего have-no-Bounded-инстанса, то получится как бы обходной манёвр, несмотря на наличие в текущем scope инстанса Bounded для MyType, я буду использовать proxy-функции из соседнего модуля, где этого Bounded нету.

                                                  Я правильно понял, что ваш инстанс не сработает для типа, у которого в scope есть также инстанс Bounded? Т.е. если другого инстанса нет, будет ошибка?

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

                                                  Я не говорю, что с этим будут какие-то проблемы (хотя, наверное, могут и быть, но я не буду ручаться за конкретные сценарии), я о том, что это как-то нематематично что ли, как и Overlapped/Undecidable, в общем-то.
                                                    0
                                                    А, я вас понял, наконец!

                                                    Функцию звать, получается, можно, да. Это всё ведь детали реализации. Более того, если я правильно вас понимаю, вместо костылей с дублированием API вы можете просто взять и написать newtype.

                                                    На самом деле в моей задаче Overlapped не подходил, хотя инстанс был и для Bounded-варианта. Я, к сожалению, напрочь забыл подробности, но мне хотелось тайпкласс LowerBound a с методом lower :: a, который был бы minBound для Bounded и -\infty для Real. Не помню, почему у меня Overlapped не хватало, и откуда вылезала необходимость в отрицании тайпкласса :(
                                        0

                                        В gcc 6.1 концепты уже есть из TS спецификации.
                                        Нужно добавить следующие флаги для компиляции:


                                        -fconcepts -std=c++1z

                                        Пример http://coliru.stacked-crooked.com/a/a59567cac9c7681d
                                        Документация http://en.cppreference.com/w/cpp/language/constraints

                                          +1

                                          Есть. Но необходимость в костылях на велосипедном приводе полностью отпадёт только тогда, когда это будет во всех компиляторах, и во всех них оно будет работать одинаково и включаться одними флагами компиляции.


                                          Всё-таки TS и стандарт — разные вещи.

                                            –1

                                            Зачем вам во всех компиляторах? Вы же на каком то конкретном работаете, а не на всех сразу.

                                              +2

                                              Странный вопрос.


                                              Во-первых, допустим, я хочу писать переносимый код.
                                              Во-вторых, допустим, я хочу в бою использовать GCC, а для статического анализа — Clang.
                                              В-третьих, чем больше компиляторов с выкрученными на максимум предупреждениями скомпилируют код, тем больше я буду в нём уверен.


                                              Ну и т.д.

                                                0

                                                Просто интересно как у вас проект собирается что вы с такой легкостью компиляторы меняете.

                                                  +2

                                                  Обычный Cmake.


                                                  ¯\_(ツ)_/¯

                                                  0
                                                  Переносимый код для меня — это когда код не привязан к нестандартным особенностям конкретного компилятора.
                                                  Ну, формально переносимый код может даже ничем не собираться, и у меня был опыт написания такого кода, потому что в gcc одни баги, в clang — другие :)

                                                  А в бою, как по мне, clang уже генерит более оптимизированный код. На днях с автовекторизатором баловался — в моей задаче clang смог больше вещей раскрутить и более оптимизированный ассемблер выдать. Но это так, конечно же, YMMV.
                                                    0
                                                    Переносимый код для меня — это когда код не привязан к нестандартным особенностям конкретного компилятора.

                                                    Ну так код из публикации и не привязан ни к каким нестандартным особенностям.


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

                                                    В моих проектах гэцэцэ побыстрее.


                                                    Хотя на простых испытательных примерах кланг действительно частенько генерирует более оптимальный ассемблерный код.


                                                    YMMV

                                                    Чё?

                                                      0
                                                      Ну так код из публикации и не привязан ни к каким нестандартным особенностям.

                                                      Да, но
                                                      Зачем вам во всех компиляторах? Вы же на каком то конкретном работаете, а не на всех сразу.

                                                      Если этот компилятор поддерживает стандарт на должном уровне, то и хорошо, разве нет?

                                                      Хотя на простых испытательных примерах кланг действительно частенько генерирует более оптимальный ассемблерный код.

                                                      Ну, не сказал бы, что у меня простые испытательные примеры. Циклы не самые тривиальные, дешугаринг сделать надо не самый тривиальный от всех этих итераторов, границы циклов известны не всегда, операции доступа памяти скрыты за operator() и шаблонными стратегиями лейаута матриц в памяти…
                                                      Я был приятно удивлён.

                                                      Чё?

                                                      Your mileage may vary.
                                            0
                                            Аккуратно, грязь:
                                            template<typename T>
                                            void foo(const T&& a)
                                            {
                                                auto&& _a = const_cast<T&&>(a);
                                                _a = +100500;
                                            }
                                            
                                              0

                                              Ну и как таким способом отличить ссылку на rvalue от ссылки на const rvalue?

                                                0
                                                Зачем их отличать? Целью функции заявлено получение владения. Что пришло — то уже не ушло, и не важно как было передано.
                                                Да и при каких разумных обстоятельствах можно получить const rvalue?

                                            Only users with full accounts can post comments. Log in, please.