Как стать автором
Обновить

Комментарии 53

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

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


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

Почему?

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

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

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

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

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

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

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");
}


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

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


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

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

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

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

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

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

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

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


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

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


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


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

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

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

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


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

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

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

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


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


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


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


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


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

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

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

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

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;
^
НЛО прилетело и опубликовало эту надпись здесь
Когда вы в статье упомянули о том, что нужно сообщать об ошибке компиляции, мне сразу подумалось, что проблему можно решить через static_asser, как уже предлагали выше. Например вот так:

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

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


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


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


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

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

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

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


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

НЛО прилетело и опубликовало эту надпись здесь
реализовать тайпкласс для всех вещей, не реализующих тайпкласс Bounded

А это как? Создал тип данных, импортнул ваш модуль — и инстанс тайпкласса есть, могу им воспользоваться в функции. Добавил в соседнем инстанс Bounded — и в нём инстанса вашего тайпкласса уже нет, но функцию мою, к примеру, можно звать?
НЛО прилетело и опубликовало эту надпись здесь
У вашей функции будет же сигнатура вроде ¬(Bounded a) => a -> foobar, разве нет?

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

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

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

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

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

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

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

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


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

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

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


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


Ну и т.д.

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

Обычный Cmake.


¯\_(ツ)_/¯

НЛО прилетело и опубликовало эту надпись здесь
Переносимый код для меня — это когда код не привязан к нестандартным особенностям конкретного компилятора.

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


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

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


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


YMMV

Чё?

НЛО прилетело и опубликовало эту надпись здесь

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

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

Публикации

Истории