LINUX.ORG.RU

Вопрос по избыточности «possible implementation» std::is_base_of<> @ cppreference.com

 ,


0

2

Тыц / Possible implementation: зачем нужны оба test_pre_is_base_of()? Сначала не понял про первый, тупо закомментил его – и вроде всё работает. Потом заинлайнил второй – и опять работает:

namespace details {
	template <typename B> std::true_type  test_pre_ptr_convertible(const volatile B*);
	template <typename>   std::false_type test_pre_ptr_convertible(const volatile void*);
}
template <typename Base, typename Derived> struct is_base_of : std::integral_constant<
		bool,
		std::is_class<Base>::value && 
		std::is_class<Derived>::value && 
		decltype(details::test_pre_ptr_convertible<Base>(static_cast<Derived*>(nullptr)))::value
> {};
class A {};
class B {};
class D : public B {};
static_assert(is_base_of<B, D>::value);
static_assert(!is_base_of<A, D>::value);
static_assert(!is_base_of<int, bool>::value);

★★★★★

Последнее исправление: dimgel (всего исправлений: 1)

Сначала не понял про первый, тупо закомментил его – и вроде всё работает.

~ % g++ main.cxx
main.cxx:17:33: error: static assertion failed
   17 | static_assert(is_base_of<B, D>::value);
      |               ~~~~~~~~~~~~~~~~~~^~~~~

Потом заинлайнил второй – и опять работает:

~ % g++ main.cxx
main.cxx: In instantiation of ‘struct is_base_of<A, D>’:
main.cxx:18:32:   required from here
main.cxx:12:51: error: cannot convert ‘D*’ to ‘const volatile A*’
   12 |   decltype(details::test_pre_ptr_convertible<Base>(static_cast<Derived*>(nullptr)))::value
      |            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      |                                                   |
      |                                                   D*
main.cxx:5:65: note:   initializing argument 1 of ‘std::true_type details::test_pre_ptr_convertible(const volatile B*) [with B = A; std::true_type = std::integral_constant<bool, true>]’
    5 | plate <typename B> std::true_type  test_pre_ptr_convertible(const volatile B*);
      |                                                             ^~~~~~~~~~~~~~~~~

main.cxx:18:34: error: ‘value’ is not a member of ‘is_base_of<A, D>’
   18 | static_assert(!is_base_of<A, D>::value);
      |                                  ^~~~~
main.cxx: In instantiation of ‘struct is_base_of<int, bool>’:
main.cxx:19:37:   required from here
main.cxx:12:51: error: cannot convert ‘bool*’ to ‘const volatile int*’
   12 |   decltype(details::test_pre_ptr_convertible<Base>(static_cast<Derived*>(nullptr)))::value
      |            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      |                                                   |
      |                                                   bool*
main.cxx:5:65: note:   initializing argument 1 of ‘std::true_type details::test_pre_ptr_convertible(const volatile B*) [with B = int; std::true_type = std::integral_constant<bool, true>]’
    5 | plate <typename B> std::true_type  test_pre_ptr_convertible(const volatile B*);
      |                                                             ^~~~~~~~~~~~~~~~~

main.cxx:19:39: error: ‘value’ is not a member of ‘is_base_of<int, bool>’
   19 | static_assert(!is_base_of<int, bool>::value);
      |                                       ^~~~~

Компилятор выбирает между двумя перегрузками, выбирая наиболее подходящую. Если Derived наследуется от B, то будет выбрана первая перегрузка, так как указатель на Derived можно безопасно привести к указателю на B, в противном случае будет выбрана перегрузка с void.

Siborgium ★★★★★
()
Ответ на: комментарий от Siborgium

А у меня всё чисто – и ClangCodeModel в QtCreator, и сборка проекта gcc-10.2.0-r5 (MinGW).

UPD: –std=c++2a

Компилятор выбирает между двумя перегрузками, выбирая наиболее подходящую.

Это ты про первые две строки – test_pre_ptr_convertible(); с ними-то как раз всё ясно. А вопрос был – зачем по ссылке нужны ещё две test_pre_is_base_of(). Первая – которая с аргументами «(…)» и возвращающая true_type – вообще дичь какая-то. Вторую я, повторюсь, тупо заинлайнил.

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 1)
Ответ на: комментарий от dimgel

А, я-то по ссылке не ходил :)

Это deduction guide. Первый не используется, второй тоже по большому счету не нужен, проще было, как ты и написал, просто заинлайнить.

cppreference тоже люди пишут.

Siborgium ★★★★★
()
Последнее исправление: Siborgium (всего исправлений: 1)
Ответ на: комментарий от Siborgium

(чёт я пересуетился с удалением каментов) Сенькс. Про deduction guide щас пока не буду грузиться.

dimgel ★★★★★
() автор топика
Ответ на: комментарий от dimgel

один чел написал «in order to call other existing tests like std::is_fundamental»

Если я правильно понял, он имел в виду, что было бы хорошо вызывать std::is_fundamental перед std::is_class (зачем? читаемости ошибок это не добавит). Непосредственно в deduction guide происходит следующее:

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

  2. Подставление нужных аргументов в test_pre_ptr_convertible.

  3. Возврат вычисленного test_pre_ptr_convertible типа.

Учитывая все вышесказанное, и что вычисленный тип нужен только для ::value от него, проще было бы написать что-то типа

template <typename B, typename D>
constexpr bool test_is_base_of = decltype(
    details::test_pre_ptr_convertible<B>(static_cast<D*>(nullptr))
)::value;
Siborgium ★★★★★
()
Последнее исправление: Siborgium (всего исправлений: 1)

Я не смог разобраться что там за магия с test_pre_is_base_of происходит, но твой вариант не работает с protected и private наследованием в отличие от стандартной реализации.

Vinick ★★
()
Ответ на: комментарий от Vinick

но твой вариант не работает с protected и private наследованием в отличие от стандартной реализации.

Хм, а должен? С точки зрения LSP, protected и private-подклассы не являются подтипами.

dimgel ★★★★★
() автор топика
Ответ на: комментарий от dimgel

Вообще должен.

std::is_base_of<A, B>::value is true even if A is a private, protected, or ambiguous base class of B. In many situations, std::is_convertible<B*, A*> is the more appropriate test.

Судя по всему, я погорячился, и deduction guide здесь для обхода этой проблемы. В правилах выведения есть такая строчка:

Substitute the result of above deduction into f, if substitution fails, no guide is produced;

Т.е., как я понял, если гайд с int не удается вывести, используется второй гайд (с ...).

Siborgium ★★★★★
()
Ответ на: комментарий от Siborgium

Пфффф… Пощадите мой межушный ганглий. :) Попозже ещё раз попробую разобраться, с учётом вновь открывшихся обстоятельств. :) С ходу нифига не понятно, как эти гайды решают проблему.

@Vinick - спасибо за наводку.

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 1)
Ответ на: комментарий от anonymous

Хм, а прикольно: беглое гугление показало, что в расте есть AST-макросы. В принципе, это первый аргумент «за», который мне видится существенным.

dimgel ★★★★★
() автор топика
Ответ на: комментарий от Siborgium

Про наследование – чёт не верится. Ну а называть этот сраный плюсовый изврат (костыль на костыле, начиная с самого фундамента – пишем «struct», читаем «функция») метапрограммированием – может только человек, метапрограммирования вообще не видевший. Уж извините. :) Даже эти read-only инспекции типа is_base_of (и гораздо более сложные, фактически произвольно сложные) на AST-макросах делаются тупым, прямолинейным, явным, императивным инспектированием AST-узлов, безо всяких диких нечитабельных хаков.

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 1)
Ответ на: комментарий от dimgel

Про наследование – чёт не верится.

https://internals.rust-lang.org/t/why-not-inheritance/5738

метапрограммированием – может только человек, метапрограммирования вообще не видевший

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

Конкретно AST-макросы – хорошая вещь, вот только в Rust не они. В Rust используются «обычные» макросы и proc_macro – функции на Rust, оперирующие потоками токенов. Это уже лучше, чем замена по голому тексту, но все еще убого.

https://doc.rust-lang.org/book/ch19-06-macros.html

https://doc.rust-lang.org/reference/procedural-macros.html

https://doc.rust-lang.org/proc_macro/enum.TokenTree.html

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

Siborgium ★★★★★
()
Последнее исправление: Siborgium (всего исправлений: 3)
Ответ на: комментарий от Siborgium

Уродливость крестового метапрограммирования в целом вытекает из отсутствия рефлексии в стандарте.

За compile-time рефлексию в плюсах я вроде слышал всякое шушушу. Она может помочь при read-only инспекциях, аналогично как java reflection используется в рантайме. Но:

(1) На java я юзаю reflection из императивного кода, и могу реализовывать произвольную логику на привычном языке в привычном синтаксисе (а не pure functional в синтаксисе идиотском) – так что compile-time-аналогом были бы опять же императивные AST-макросы.

(2) Упомянутый тобой __is_base_of уже можно рассматривать как сырое-корявое-неполное reflection API, так что с введением не корявого и полного, принципиально ничего не изменится.

(3) А самое главное – не в read-only инспекциях, а в возможностях кодогенерации. И здесь плюсовое «метапрограммирование» отсасывает полноценной императивной кодогенерации с таким свистом, что даже и обсуждать нечего. Первый же возникший в голове простейший пример – добавлять поле в класс по условию – в плюсах делается ожидаемо через жопу. Как впрочем и всё остальное. В макросе же я пишу, условно: if (условие) classNode.add(new FieldNode(…)).

dimgel ★★★★★
() автор топика
Ответ на: комментарий от Siborgium

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

Строго наоборот. Скаловские макросы пишутся на скале, жавовские (lombok) – на жаве. А вот плюсовые используют возможности языка, вообще для этого изначально не предназначенные, и получается как раз-таки отдельный язык, причём корявый и костыльный.

dimgel ★★★★★
() автор топика
Ответ на: комментарий от Siborgium

Т.е., как я понял, если гайд с int

Нет там гайдов. Это просто объявления функций с trailing type. Гайды это про типы и вывод шаблонных параметров класса из аргументов конструктора.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Это просто объявления функций с trailing type.

И как этот синтаксический сахар помогает достучаться до private-базовых классов, again?

dimgel ★★★★★
() автор топика
Ответ на: комментарий от xaizek

Принято, пойду делать зарядку для глаз. auto я и не приметил…

Siborgium ★★★★★
()
Ответ на: комментарий от dimgel

И как этот синтаксический сахар помогает достучаться до private-базовых классов, again?

Второй test_pre_is_base_of зафейлится на касте (он пройдёт, но будет ошибка) и выберется первый вариант (ибо SFINAE), который вернёт true.

Из libstdc++:

  template<typename _Base, typename _Derived>
    struct is_base_of
    : public integral_constant<bool, __is_base_of(_Base, _Derived)>
    { };

А без встроенной функции надо извращаться.

xaizek ★★★★★
()
Последнее исправление: xaizek (всего исправлений: 2)
Ответ на: комментарий от dimgel

(1), (3)

Про Java я вообще ничего не говорил, речь шла про ущербность растовых макросов.

(2)

Ну как не изменится – не нужно будет писать вот такой код.

Строго наоборот. Скаловские макросы пишутся на скале, жавовские (lombok) – на жаве.

…А растовые proc_macro на расте. Какая разница, если в основной программе нет никакого доступа к макросовым внутренностям?

Как-то так выглядит рефлексия здорового человека приболевшего человека https://godbolt.org/z/sqbo4qPv7

И как этот синтаксический сахар помогает достучаться до private-базовых классов, again?

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

Siborgium ★★★★★
()
Ответ на: комментарий от xaizek

Второй test_pre_is_base_of зафейлится на касте (он пройдёт, но будет ошибка) и выберется первый вариант (ибо SFINAE), который вернёт true.

Сенькс. Мелькала у меня такая же мысль. Но тут же возникал встречный вопрос: почему первый вариант не выберется на вообще не связанных типах?

А без встроенной функции надо извращаться.

Гы, а вот щас наконец-то съязвлю. Но не в твой огород, а огород @Siborgium, т.к. он успел подставиться, с одной стороны воспевая/защищая плюсовое МП, а с другой – тоже признавал что это всё хаки:

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

dimgel ★★★★★
() автор топика
Ответ на: комментарий от Siborgium

…А растовые proc_macro на расте.

Именно это я ранее и нагуглил. Причём пример кодогенерации. И это есть хорошо.

Какая разница, если в основной программе нет никакого доступа к макросовым внутренностям?

А он должен быть? :) Как разновидность метапрограммировния – генерация исходников отдельной программой, запускаемой перед компиляцией (пример, генерация классов сущностей по структуре базы); и какой доступ должен быть у сгенерированной программы к генератору?

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 1)
Ответ на: комментарий от dimgel

воспевая/защищая плюсовое МП

Я не воспеваю плюсовое МП, это уродство. Но это много лучше МП раста.

Гы, а вот щас наконец-то съязвлю.

А теперь перечитай еще раз сообщение и подумай, где ты ошибся.

Но тут же возникал встречный вопрос: почему первый вариант не выберется на вообще не связанных типах?

Связанность типов проверяется уже внутри второго варианта. Выбор между вариантами (test_pre_is_base_of) осуществляется на основе аргумента 0, а не на основе шаблонных параметров.

Siborgium ★★★★★
()
Последнее исправление: Siborgium (всего исправлений: 1)
Ответ на: комментарий от Siborgium

Но это много лучше МП раста.

Если в расте – полноценная манипуляция AST, то лучше быть не может.

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

А теперь перечитай еще раз сообщение и подумай, где ты ошибся.

Да хрен его знает, вроде нигде… :)

Связанность типов проверяется уже внутри второго варианта.

Дык второй вариант будет молча выкинут по SFINAE, если связанности нет?

UPD:

Выбор между вариантами (test_pre_is_base_of) осуществляется на основе аргумента 0, а не на основе шаблонных параметров.

Непонятно. НЯЗ, «(…)» удовлетворяет любым аргументам, но имеет самый низший приоритет; так что когда вариант (2) будет выкинут по SFINAE, вариант (1) всё равно отработает для любых типов аргументов.

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 2)
Ответ на: комментарий от dimgel

почему первый вариант не выберется на вообще не связанных типах?

У ... приоритет при разрешении перегрузок самый маленький именно потому что он принимает всё.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

У … приоритет при разрешении перегрузок самый маленький именно потому что он принимает всё.

Это понятно. И что ему мешает выбраться на не связанных типах, после того как второй вариант отброшен?

dimgel ★★★★★
() автор топика
Ответ на: комментарий от dimgel

То, что второй вариант подходит лучше (0 это int) и будет выбран всегда, если не произойдёт ошибки.

xaizek ★★★★★
()
Ответ на: комментарий от dimgel

Второй вариант вернет true, если тип D унаследован публично от B и false, если не унаследован никак. Если же наследование protected/private, то произойдет ошибка, и только тогда будет рассматриваться первый вариант.

Siborgium ★★★★★
()
Ответ на: комментарий от Siborgium

@Siborgium, @xaizek: Спасибо. В общем, затык у меня был из-за расхождения интуитивных ожиданий с реальным поведением: из overloaded-версий f(Base*) и f(void*), для public-подкласса выбирается f(Base*), для ваще левого класса выбирается f(void*), а для private-подкласса вместо того, чтобы выбрать f(void*), ругается «не могу кастануть к private-суперклассу».

dimgel ★★★★★
() автор топика
Ответ на: комментарий от Siborgium

А вот теперь продолжим срач! =) Я тут повспоминал, что я делал на макросах.

Первое большое, что вспомнилось – это статически-типизированный DSL, транслирующийся в SQL не в рантайме, а во время компиляции макросами. На скале: и сам делал, и в опенсорсе лежит как минимум одно решение. Но потом вспомнил, что для плюсов есть sqlpp, который тоже транслирует в compile-time. Не помню уже, по какой причине я его забраковал (что-то там было то ли дюже громоздко, то ли дюже коряво), но не суть; будем считать что аналог есть.

А потом вспомнил целый огроменный пласт, которому аналогов на плюсовом «МП» не сделать в приниципе: AOP. Потому что тут переписывется тело метода. На жаве (JEE, Spring, Hibernate) оно интенсивно используется для заворачивания функций в обёртки, управляющие транзакциями, логированием, ленивой загрузкой при обращению к полю сущности и т.п. – в рантайме. В результате серверные жава-приложения стартуют медленно, и стек-трейсы исключений у них неприлично огромные (на каждую спринг-обёртку приходится несколько вызовов).

Так вот, доводилось мне делать свою lombok-аннотацию @Transactional, обрабатываемую соответственно в compile-time. Просто потому что мне нужно было протаскивать доп. параметр, который в стандартных @Transactional отсутствовал.

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

А на макросах в compile-time я переписываю тело метода (в lombok это AST-узел Block, содержащий список statements, т.е. «{ … }»), помещая этот Block внутрь собственного кода. Совсем грубо, было:

// Наличие lombok-аннотации -- триггер, запускающий мою обработку.
// В скале есть макро-аннотации и макро-функции.
@Transactional
R f(A a, B b, C c) {
   ab = a + b;
   return ab - c;
}

стало:

R f(A a, B b, C c) {     // Сигнатуру не трогаю.
   startTransaction();
   try {                 // Исходное тело метода {...} не трогаю.
      ab = a + b;
      return ab - c;
   } catch (Exception e) {
      rollback();
      throw e;
   } finally {
      commit();
   }
}

Этот пример слишком схематичный и корявый: тут муть с commit() после rollback(). В реале мне, чтобы вообще не касаться и сигнатуры, ни тела метода (не анализировать, не переписывать, в т.ч. не подсовывать commit() перед каждым return), пришлось делать два вложенных try.

UPD. И ещё у меня там был цикл по попыткам при дедлоках.

Ну так вот: ЭТО – метапрограммирование. А в плюсах – пое@$#ь.

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 1)
Ответ на: комментарий от dimgel

Если в расте – полноценная манипуляция AST, то лучше быть не может.

По умолчанию процедурный макрос получает только TokenStream, то есть итератор на лексемы. AST нужно строить или самим или внешней библиотекой, такая, почти что стандартная, есть - это https://github.com/dtolnay/syn

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

Оно есть, но тоже не является встроенным в язык, а само реализовано в виде макроса https://github.com/dtolnay/quote

В итоге, если не пользоваться сторонними крейтами, то на самом деле метапрограммирование на расте, как заметили выше достаточно убого, но даже это по моему вполне может, кроме тривиальных случаев и захардкоженного сахара переплевывать шаблоны С++, вообще по своей мощности близко к строковым миксинам D, но строже их. Если же брать крейты syn и quote, то уже по удобству вполне близко у макросам скалы или немерле (конечно более ограниченно процедурным видом). Но есть и недостаток, что такая реализация замедляет намного больше компиляцию чем если бы было встроено в компилятор.

anonymous
()
Ответ на: комментарий от anonymous

Ясно.

По умолчанию процедурный макрос получает только TokenStream, то есть итератор на лексемы. AST нужно строить или самим

Плохо. :)

или внешней библиотекой, такая, почти что стандартная, есть - это https://github.com/dtolnay/syn

Хорошо. :)

Не принципиально, на самом деле, встроенная или внешняя. Лишь бы работала. Хотя я так понимаю, типизированное дерево она не даст, – и вот это уже реально плохо. Хотя, помнится я юзал JavaParser (прочитал сорц, раскорёжил, записал назад) без типизации (хотя в нём есть) – и был в полнейшем восторге от его элегантности и удобства.

dimgel ★★★★★
() автор топика
Ответ на: комментарий от dimgel

в т.ч. не подсовывать commit() перед каждым return

Эх, а мог бы переименовать исходный метод в private f_$insideTransaction() какой-нибудь, а под исходным именем создать проксю с точно такой же сигнатурой – и вложенные try не нужны были бы, и транзакция в стек-трейсе одной строкой засветилась бы (а то её вообще не видно, она полностью внутри метода). Т.е. была бы прокся, но статически-типизированная, без медленных вызовов проксируемой функции через рефлексию, и без добавления прокси-классов. Хорошая мысля приходит годы опосля.

dimgel ★★★★★
() автор топика
Ответ на: комментарий от dimgel

А вот теперь продолжим срач! =)

Не получится – я на Java ничего серьезнее калькулятора не писал. Да и спор бесполезный: я анонимусу (и частично вам) про кресты/раст, вы мне про джаву и скалу. Если очень хочется, вот тезис: в хорошем языке кодогенерация внутренняя.

которому аналогов на плюсовом «МП» не сделать в приниципе: AOP.

Да, у стандартных плюсов возможности кодогенерации такого рода ограничены. Если интересно, посмотрите Cppx, ссылку на который я кидал выше – это пример того, что можно было бы делать в крестах, если бы это приняли в стандарт это хотя бы допилили до production-ready состояния.

Есть еще такое: https://godbolt.org/z/Yc8Kbqrxb

Siborgium ★★★★★
()
Последнее исправление: Siborgium (всего исправлений: 2)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.