хочуфичу - функции-конструкторы
От: Кодт Россия  
Дата: 27.12.23 17:46
Оценка:
Распространённая идиома на плюсах (на сях она вынужденная) — это мейкфункция: декоратор конструктора.
class Foo {
public:
  Foo(Bar);
  Foo(Buz, Xyz);
  ...
};

// руками написать
Foo make_foo(auto&&... args) { return Foo(std::forward<decltype(args)>(args)...); }

// стандартными воспользоваться
std::optional<Foo> o(std::in_place, .....);
auto u = std::make_unique<Foo>(.....);
auto s = std::make_shared<Foo>(.....);


Которая плоха тем, что при несоответствии типов аргументов формирует громоздкие ошибки.

Вот было бы здорово как-то говорить компилятору "этот набор аргументов — такой же, как для вот такого семейства функций"
template<class T, class... Args> shared_ptr<T> make_shared(Args&&... args) [[signature_like ( T::T(Args&&...) ) ]]

чтобы потом компилятор говорил это юзеру.


Может, у кого-нибудь ещё есть схожие хотелки вокруг этого? Чтоб помечтать к следующему стандарту?
Перекуём баги на фичи!
Re: хочуфичу - функции-конструкторы
От: rg45 СССР  
Дата: 27.12.23 18:06
Оценка: +1
Здравствуйте, Кодт, Вы писали:

К>Которая плоха тем, что при несоответствии типов аргументов формирует громоздкие ошибки.


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

К>
К>template<class T, class... Args> shared_ptr<T> make_shared(Args&&... args) [[signature_like ( T::T(Args&&...) ) ]]
К>

К>чтобы потом компилятор говорил это юзеру.

Так а констрейнты чем плохи?

http://coliru.stacked-crooked.com/a/a1a066dba3fa77e9

template <typename T, typename...A>
requires requires(A&&...args) { T{std::forward<A>(args)...}; }
std::shared_ptr<T> make_shared(A&&... args) { return std::make_shared<T>(std::forward<A>(args)...); }

int main()
{
    make_shared<Foo>(Bar{});
    make_shared<Foo>(Buz{}, Xyz{});
    // make_shared<Foo>(42); // error: no matching function for call to 'make_shared<Foo>(int)'
}


Можно даже не полениться и объявить концепт:

http://coliru.stacked-crooked.com/a/a288792cc15b585f

template <typename T, typename...A>
concept Constructible = requires(A&&...args) { T{std::forward<A>(args)...}; };

template <typename T, typename...A>
requires Constructible<T, A...>
std::shared_ptr<T> make_shared(A&&... args) { return std::make_shared<T>(std::forward<A>(args)...); }
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 27.12.2023 18:38 rg45 . Предыдущая версия . Еще …
Отредактировано 27.12.2023 18:26 rg45 . Предыдущая версия .
Отредактировано 27.12.2023 18:26 rg45 . Предыдущая версия .
Отредактировано 27.12.2023 18:18 rg45 . Предыдущая версия .
Отредактировано 27.12.2023 18:13 rg45 . Предыдущая версия .
Отредактировано 27.12.2023 18:09 rg45 . Предыдущая версия .
Отредактировано 27.12.2023 18:07 rg45 . Предыдущая версия .
Re: хочуфичу - функции-конструкторы
От: Voivoid Россия  
Дата: 27.12.23 18:47
Оценка:
Здравствуйте, Кодт, Вы писали:

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


Да вроде можно просто static_assert добавить, что-нибудь типа:
static_assert(std::is_constructible_v<Foo, Args...>);

например https://godbolt.org/z/Wh1hjET9s

Что правда не мешает компилятору даже после фейла static_assert'а все равно и дальше пытаться компилять и соотственно генерировать дальнийшие громоздкие ошибки. Но тут -Wfatal-errors может помочь
Re[2]: хочуфичу - функции-конструкторы
От: Кодт Россия  
Дата: 27.12.23 20:55
Оценка:
Здравствуйте, rg45, Вы писали:

R>Так а констрейнты чем плохи?

R>Можно даже не полениться и объявить концепт:

Уже украдено до нас: std::is_constructible_v в <type_traits> и std::constructible_from в <concepts>

Плохи тем, что неудача подстановки in vivo приводит к читаемой ошибке, где перечисляются неудачные кандидаты.
Другое дело, что читаемость у shared_ptr закопана в недра — куда он тщетно пробрасывает аргументы, чтобы сконструировать объект специальным аллокатором.
А у optional она разбавлена другими конструкторами, которые тоже не подошли.

Тогда как с констрейном (хоть на requires, хоть на enable_if) она сведётся к простой фразе "нешмогла".
Перекуём баги на фичи!
Re[2]: хочуфичу - функции-конструкторы
От: Кодт Россия  
Дата: 27.12.23 21:18
Оценка: 2 (1)
Здравствуйте, Voivoid, Вы писали:

V>Да вроде можно просто static_assert добавить, что-нибудь типа:


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

Когда у него голая неудача — он пишет причину неудачи
— не подошёл ни один кандидат из "вот посмотрите что я пробовал"
— неоднозначный выбор из "вот посмотрите что подошло"
— что-то ещё

А когда неудача завёрнута в булево выражение? "false".

Кстати, прикольно! Стандартный концепт constructible_from у gcc реализован через предикат is_constructible_v. Ну false и false.

А вот если написать руками
template<class T, class... A>
concept Constructible = requires(A... a) { T(a...); };

и по наущению gcc выставить ему флажок -fconcepts-diagnostics-depth=2
то он провалится внутрь констрейна и распишет, что именно ему не понравилось — голую неудачу.

https://gcc.godbolt.org/z/Wb39YWcdv

Но clang и msvc так в принципе не умеют. Ну false и false.

Прикольно, например, если передать там вторым аргументом nullptr, то подойдёт сигнатура foo(double, bool), но будет ошибка

converting to 'bool' from 'std::nullptr_t' requires direct-initialization [-fpermissive]


А попробуй догадайся без подсказки, что там сломалось?! От то-то же.
Перекуём баги на фичи!
Re[3]: хочуфичу - функции-конструкторы
От: reversecode google
Дата: 27.12.23 21:44
Оценка:
любопытно
вроде стандарт вроде как не регламентирует концепты на аргументы итд
это воля разработчиков либ

если да
то это и через пропозл не протянуть
Re[4]: хочуфичу - функции-конструкторы
От: Кодт Россия  
Дата: 27.12.23 23:14
Оценка:
Здравствуйте, reversecode, Вы писали:

R>вроде стандарт вроде как не регламентирует концепты на аргументы итд

R>это воля разработчиков либ

R>если да

R>то это и через пропозл не протянуть

Сейчас действует такая логика:
вносить изменения в namespace std нельзя
— набор перегрузок make_unique / make_shared / optional::optional прибит гвоздями
— поэтому неудача подстановки — это в любом случае ошибка компиляции

А уж как именно эту ошибку формировать — дело десятое:
— отлуп на уровне объявления через старый добрый шаблонный sfinae
— отлуп на уровне объявления через requires
— static_assert на очень ранней стадии
— ошибка в недрах

Для пропозала здесь ещё нет места, потому что принцип замороженности std — достаточно важен.

Но вот сформулировать пропозал "давайте сделаем человекочитаемую диагностику"... было бы круто.
gcc ведь смог, при условии, что ему никто не мешает (хотя его собственная реализация стандартной библиотеки — мешает).


Ещё одно направление для пропозала... Не знаю, как точно сформулировать.
Суть вот в чём. Шаблон декоратора стирает то, как объявлены аргументы в декорируемой функции. И это может повлиять на правила выбора перегрузки.
Грубо говоря,
void f(X, Y, Z);
void f(X, Y, auto);
void f(X, auto, T) requires true;  // констрейны - это тоже часть сигнатуры (хотя в ABI и не проросло ещё?)

// и декоратор
void g(auto... a) /* [[signature_like (f(a...))]] */ ;
void g(X, U, V);

Без вытаскивания перегрузок — тут сработает очень тупое правило:
— если типы аргументов не совпадают с точностью до cv& с X,U,V — то лучше подходит шаблон
— а в рамках этого шаблона могут быть и неоднозначности, и наоборот, ничего не подойдёт

То есть, вместо плоского семейства перегрузок мы получаем иерархическое. И логика из-за этого меняется.

Вроде бы в прошлом или позапрошлом году я уже развлекался с темой "как делать полиморфную функцию, диспетчеризующую россыпь лямбд".
Есть такой лайфхак (прикроем глаза на то, что типы должны различаться — это легко сделать, но обвязка будет громоздкой)
template<class... Fs> struct join : Fs... {
  Fs... fs_;
  joined(Fs... fs) : Fs(fs)... {}
  using F::operator();  // <-- ключевой момент
};

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

А попробуйте подверстать аргумент, или, напротив, связать его? Ну положим, мы можем адаптировать каждую лямбду независимо
auto replace_first_with_int(auto f) {
  return [f](int x, auto... a) requires requires { f(x, a...); } { f(x, a...); };
}

и потом склеить эти адаптеры
auto join_replaced_first_with_int(auto... fs) {
  return join(replace_first_with_int(fs)...);
}

Вот только здесь мы прямо сразу обеспечили себе ошибку неоднозначности!
Если f1 и f2 подходят (например, у f1 тип аргумента идеально совпал, а у f2 через преобразование), то сигнатуры адаптеров — это (int, auto...) — и они подходят одинаково!

И в настоящее время я затрудняюсь сказать, как это можно красиво решить. И можно ли вообще.
Перекуём баги на фичи!
Отредактировано 27.12.2023 23:15 Кодт . Предыдущая версия .
Re[3]: хочуфичу - функции-конструкторы
От: rg45 СССР  
Дата: 29.12.23 10:40
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Но clang и msvc так в принципе не умеют. Ну false и false.


msvc очень даже умеет! Если взять вот такой пример:

  код примера
#include <memory>
#include <utility>

template <typename T, typename...A>
concept Constructible = requires(A&&...a) { T{std::forward<A>(a)...}; };

template <typename T, typename...A>
requires Constructible<T, A...>
std::shared_ptr<T> make_shared(A&&...a) { return std::make_shared<T>(std::forward<A>(a)...); }

struct Foo
{
   Foo(int, const char*) {}
   Foo(const char*, int) {}
   Foo(const char*, const char*) {}
};

int main()
{
   make_shared<Foo>(42, "Hello, World!"); // OK
   make_shared<Foo>("Hello, World!", 42); // OK
   make_shared<Foo>("Hello", "World!");   // OK

   make_shared<Foo>(42, 43); // error C2672: 'make_shared': no matching overloaded function found
}


и скомпилировать его в Visual Studio (не в godbolt), то компилятор формирует вот такую диагностику:

  диагностика msvc-14.3
1>E:\Development\VS2022\ConsoleApplication1\main.cpp(23,4): error C2672: 'make_shared': no matching overloaded function found
1>E:\Development\VS2022\ConsoleApplication1\main.cpp(9,20):
1>could be 'std::shared_ptr<_Ty> make_shared(A &&...)'
1>    E:\Development\VS2022\ConsoleApplication1\main.cpp(23,4):
1>    the associated constraints are not satisfied
1>        E:\Development\VS2022\ConsoleApplication1\main.cpp(8,10):
1>        the concept 'Constructible<Foo,int,int>' evaluated to false
1>            E:\Development\VS2022\ConsoleApplication1\main.cpp(5,46):
1>            '<function-style-cast>': cannot convert from 'initializer list' to 'Foo'
1>                E:\Development\VS2022\ConsoleApplication1\main.cpp(5,46):
1>                'Foo::Foo': no overloaded function could convert all the argument types
1>                    E:\Development\VS2022\ConsoleApplication1\main.cpp(15,4):
1>                    could be 'Foo::Foo(const char *,const char *)'
1>                        E:\Development\VS2022\ConsoleApplication1\main.cpp(5,46):
1>                        'Foo::Foo(const char *,const char *)': cannot convert argument 1 from '_Ty' to 'const char *'
1>        with
1>        [
1>            _Ty=int
1>        ]
1>                            E:\Development\VS2022\ConsoleApplication1\main.cpp(5,63):
1>                            Conversion from integral type to pointer type requires reinterpret_cast, C-style cast or parenthesized function-style cast
1>                    E:\Development\VS2022\ConsoleApplication1\main.cpp(14,4):
1>                    or       'Foo::Foo(const char *,int)'
1>                        E:\Development\VS2022\ConsoleApplication1\main.cpp(5,46):
1>                        'Foo::Foo(const char *,int)': cannot convert argument 1 from '_Ty' to 'const char *'
1>        with
1>        [
1>            _Ty=int
1>        ]
1>                            E:\Development\VS2022\ConsoleApplication1\main.cpp(5,63):
1>                            Conversion from integral type to pointer type requires reinterpret_cast, C-style cast or parenthesized function-style cast
1>                    E:\Development\VS2022\ConsoleApplication1\main.cpp(13,4):
1>                    or       'Foo::Foo(int,const char *)'
1>                        E:\Development\VS2022\ConsoleApplication1\main.cpp(5,46):
1>                        'Foo::Foo(int,const char *)': cannot convert argument 2 from '_Ty' to 'const char *'
1>        with
1>        [
1>            _Ty=int
1>        ]
1>                            E:\Development\VS2022\ConsoleApplication1\main.cpp(5,63):
1>                            Conversion from integral type to pointer type requires reinterpret_cast, C-style cast or parenthesized function-style cast
1>                    E:\Development\VS2022\ConsoleApplication1\main.cpp(5,46):
1>                    while trying to match the argument list '(_Ty, _Ty)'
1>        with
1>        [
1>            _Ty=int
1>        ]


В этой диагностике присутствуют интересующие нас строки:

1>    the associated constraints are not satisfied
 . . .
1>        the concept 'Constructible<Foo,int,int>' evaluated to false
 . . .
1>            '<function-style-cast>': cannot convert from 'initializer list' to 'Foo'
 . . .
1>                'Foo::Foo': no overloaded function could convert all the argument types
 . . .
1>                    could be 'Foo::Foo(const char *,const char *)'
 . . .
1>                    or       'Foo::Foo(const char *,int)'
 . . .
1>                    or       'Foo::Foo(int,const char *)'
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 29.12.2023 10:45 rg45 . Предыдущая версия .
Re[3]: хочуфичу - функции-конструкторы
От: rg45 СССР  
Дата: 29.12.23 12:34
Оценка:
Здравствуйте, Кодт, Вы писали:

К>
К>template<class T, class... A>
К>concept Constructible = requires(A... a) { T(a...); };
К>


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

http://coliru.stacked-crooked.com/a/8c51faa24ba97d64

#include <memory>

template<class T, class... A>
concept Constructible = requires(A... a) { T(a...); };

struct Foo
{
   Foo(std::unique_ptr<int>&&) {}
};

int main()
{
    static_assert( ! Constructible<Foo, std::unique_ptr<int>>);   // NOT constructible
    static_assert( ! Constructible<Foo, std::unique_ptr<int>&&>); // NOT constructible
}


Если не хочется тащить в концепт perfect forwarding ссылки, то этого можно не делать (это вопрос семантики, которую мы хотим заложить в концепт), но использовать std::forward очень желательно в любом случае:

template<class T, class... A>
concept Constructible = requires(A... a) { T(std::forward<A>(a...)); };
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 29.12.2023 12:45 rg45 . Предыдущая версия .
Re[4]: хочуфичу - функции-конструкторы
От: Кодт Россия  
Дата: 31.12.23 10:52
Оценка:
Здравствуйте, rg45, Вы писали:

R>Немного оффтопа: в таком виде этот концепт не будет работать для классов, конструируемых по rvalue ссылкам — по той причине, что a... в теле концепта являются lvalue выражениями. И не просто не будет работать, а его использование будет невозможным ни под каким соусом:


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

И вообще, жалко, что в синтаксисе нет какой-нибудь короткой формы для идиомы `std::forward<decltype(arg)>(arg)`.
rvalue-ссылки — хакнули парсер, чтоб лексер не переделывать, взяли &&.
Универсальные ссылки — хакнули семантику, чтобы парсер не переделывать, взяли опять &&.
А вот forward/move уже не парились, выкатили быстрофикс в стандартную библиотеку и успокоились.
Перекуём баги на фичи!
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.