Структуры вида struct Pair(i32, f32); служат для другой цели. Их используют когда хочется связать несколько значений под одним общим именем типа, но отдельные поля именовать не хочется.
По сути (очень грубо) это и есть тюплы. Просто это strong typedef (т.е., алиас, который кроме синонима вводит ещё и новый тип на основе которого можно делать перегрузку ф-й и прочее. То, чего не хватает плюсам на уровне языка).
struct Meters(f32) — подтверждает это. На псевдо коде мы имеем что-то такого:
Спасибо за комментарий.
Предупреждаю — ниже взгляд человека, который НЕ разбирался в языке совершенно ни грамма.
Навскидку:
name: &'static str,
этот маленький ' крючёчек для меня, как человека с плохим зрением — просто сложно заметен.
Непривычно смотрится:
&self, f: &mut Formatter
но это просто нужно один раз понять (я не разбирался что всё это значит ещё. Догадываюсь, что, возможно, в Rust-е всё const-by-default, поэтому &mut. А & — потому что, так же, как и в плюсах, есть возможность передавать по ссылке/по значению. Но опять же — это просто догадки).
Непривычно смотрится просто знак восклицания println! и всякие штуки, типа slice: &[i32].
Дальше:
// A tuple struct
struct Pair(i32, f32);
только с комментария догадываюсь, что это аналог алиаса (?): using Pair = std::tuple<i32, f32>; и, возможно, доступ будет происходить по индексам.
Не вы одни сдаётесь. Вон, Мейерс тоже сдался. Но это еще не означает, что C++ — уродливый. Вы на новые стандарты C — посмотрите. Там тоже можно за голову взяться. Это нормально для языков с историей, которые еще и развиваются
да, дело вкуса, разници нет. Лично я думаю, что static_cast() — слишком длинно, C-style каст — слишком незаметно, а странный void() — и в глаза падает (идешь гуглить, что это за void()), и не слишком длинный
если забыли конструктор, ты будет первое сообщение, если забыли деструктор — второе.
Я немного не понял вас. Вот моя логика: sizeof(T) — приведёт к ошибке компиляции, если компилятор не видет определения типа T, т.е., T — неполный тип. sizeof(T) никак не зависит от того определён для user defined типа конструктор или деструктор. Это означает, что sizeof(T) > 0всегда — для нашого случая — нужен только для того, чтобы выдать пользователю ошибку во время компиляции о том, что он забыл определить указанный класс (тип) до места его использования (кстати, поскольку sizeof(T) никогда не может быть 0м — то можно просто писать sizeof(T)).
Дальше: у вас, по сути, таких места 2: конструктор и деструктор. Ставим вопрос — может ли пользователь написать такое использование PimplPtr<Impl>, чтобы Impl был, например, определён до вызова конструктора PimplPtr<Impl> и, одновременно, не определён при вызове деструктора PimplPtr<Impl>? (или наоборот). Ответ — да, может:
Случай первый: в конструкторе тип - неопределён, в деструкторе - уже определён
Т.е., да — я вас обманул в деструкторе — для приведённого второго случая — такой assert будет полезен, прошу прощения. Но текст сообщения, всё же, немного неправильный.
По поводу делегирующего конструктора — я снова вас обманул — имел в виду, что 1й конструктор должен вызывать 2й, т.е., для вашего кода, это что-то типа:
И ещё — поскольку у вас в публичном классе есть функция:
PimplPtr(std::unique_ptr<T>&& p)
То это означает, что я могу сделать так:
PimplPtr<int>{std::unique_ptr<int>{nullptr}};
либо, более неявно — через какую-то фабрику:
PimplPtr<T>{make_T()};
где make_T() вернёт нулевой указатель.
Т.е. не хватает проверки времени выполнения инварианта указатель не nullptr — я бы добавил assert() в конструктор и ф-и get().
И, моё предвзятое мнение — в мире плюсов — для таких штук как умные указатели — пытаются убежать от неявных опеаторов преобразования: т.е. — пометить бы ещё operator ElementType() как explicit...
И… и я бы написал using ElementType = T — меньше кода и проще читается…
Извините, я увлёкся — простите за, возможно, нелепую критику — ухожу от клавиатуры :)
Спасибо
Я бы всё-же не приводил "не очень хороший" код в статье для новичков: в первом примере, раз вы явно говорите об деструкторе, то можно бы и явно прописать реализацию конструкторов/операторов присваивания, следуя хорошему тону — The rule of three/five/zero. auto_ptr я бы выбросил вообще — всё по тому же поводу — deprecated — можно просто ссылочку дать — так для истории и действительно интересующихся. Про unique_ptr, возможно, нужно было бы рассказать, почему деструктор нужно реализовывать в файле-реализации (а что с move-семантикой на этот счёт ?) — но это отклонение от темы — зачем нужна своя реализация PimplPtr-а?
Если убрать из статью вводную, то причина у вас, получается одна — "нарушение логической константности" (а точнее 2ве: "propagate_const пока не является частью стандарта"). Хорошо, но — код из статьи — не компилируется.
По поводу реализации:
static_assert — не нужен — это сделает за вас вызов конструктора в make_unique.
constexpr — зачем? Он сдесь только мешает код читать и нет случая, когда он нужен был бы.
2й конструктор, который explicit (кстати, а почему? почему и не первый ?), наверное, должен вызывать первый (Delegating constructor) — хотя бы для того, чтобы логика в конструкторах совпадала
зачем в деструкторе делать тот-же statis_assert, когда в конструкторе — она уже есть ?
почему здесь = default используется, а в примерах выше, в файле-реализации — нет ?: Widget::~Widget() = default
хорошим тоном является использование одной и той же функции — основной — для реализации других ф-й, которые дублируют код для удобства, т.е. — реализация операторов должна использовать эталонный get()
Спасибо за статью. Извините за, возможно, резкий тон. Хорошего вам дня ^_^
С использованием move(args)… компилятор попробует получить rvalue из всего переданного, и будет искать void (string &&, vector &&), опять промах. Остаётся использовать std::forward.
Если вы будете использовать std::forward() в этом контексте, то это аналогично std::move() для всех параметров. Т.е., с вашим примером, будет std::move() как для 1го аргумента, так и для второго, т.е., если была бы ещё перегрузка moveNamedVector(string&&, vector&&), то вызвалась бы именно она, независимо от того использовали ли бы вы std::move() (более идиоматично) либо std::forward() (нестандартное использование).
there are technical issues with storing an allocator in a type-erased context and then recovering that allocator later for any allocations needed during copy assignment.
#include <cstdio>
#include <utility>
struct UserType
{
};
// (1)
void Concrete(UserType&&)
{
std::puts("Concrete(UserType&&)");
}
// (2)
void Concrete(UserType&)
{
std::puts("Concrete(UserType&)");
}
template<typename T>
void DoForward(T&& parameter)
{
Concrete(std::forward<T>(parameter));
}
int main()
{
UserType value;
// 1: аргумент (@value) у нас T& (UserType&, lvalue),
// а параметр (@parameter) - T&& - происходит наложение
// & + && -> получаем UserType&.
// Вызывается (2)я версия Concrete()
DoForward(value);
// 2: Передаём временный обьект -
// аргумент (@value) у нас T&& (UserType&&, грубо говоря, rvalue),
// а параметр (@parameter), всё тот же, T&& - происходит наложение
// && + && -> получаем UserType&&.
// Вызывается (1)я версия Concrete()
DoForward(UserType{});
}
parameter в DoForward(), поскольку это шаблонная функция, попадает в (не знаю как перевести) "deduced context" — и всё что в main-е я написал, это, как раз таки, "вывод типа" параметра, который учитывает "шаблонный" тип параметра и тип аргумента функции. Из-за того, что это шаблон, как видно, мы можем получить либо rvalue либо lvalue. Всё просто: "deduced context" — юзаем forward().
Как работает forward(): для первого вызова в main-е, как я уже написал, тип parameter вывелся в UserType&, т.е., T — это UserType&. forward() принимает аргументом (всегда!) — именованную переменную — т.е., это всегда lvalue. Получаем, что forward() имеет на вход шаблонный аргумент типа UserType& и тип параметра, так же, UserType& — всё это означает, что переданный на вход аргумент — это lvalue! forward() ничего не делает.
Аналогично, для второго вызова в main-е, тип parameter вывелся в UserType&&, т.е., T — это UserType&&. Получаем, что forward() имеет на вход шаблонный аргумент типа UserType&& и параметр UserType& — переданный на вход аргумент — это rvalue! forward() делает move().
Во всех остальных случаях — у нас нет "deduced context-а" и всё, каким мы его видим, таким и есть — т.е. T&& — это rvalue — нужно делать move(). T& — это ссылка — делаем что хотим, а T — хм, переменная, которая, больше не используется, поэтому можно сделать move().
для параметра функции call() нет вывода типа, потому что он уже выведен для класса в целом (мы его указываем при инстанциировании шаблона: NonDeducedContext<UserType>). Т.е., parameter — это value type — это копия аргумента функции call(). Ниже, по коду, он нигде не используется, поэтому я спокойно его муваю, тем самым говоря, что я его больше не использую и "делайте со мной что хотите".
Просто хочу заметить, что "если не сильно менять и причёсывать" код из статьи, то, как раз таки, std::forward() там нигде не нужно, просто потому что форвардинг ссылки нигде и не используются (а должны! К сожалению, и std::move() упущен… ладно).
По поводу вашей реализации, извините, я не сильно всматривался, вам не хватило аллокатора который есть в интерфейсе std::function?
Плюс, по поводу std::forward(), в этом месте — кхм, навскидку не понял, почему там std::forward(), а не std::move()? Судя по этому месту, я дико извиняюсь, — вы неправльно используете std::forward()?
Хм, мне кажется, я начинаю завидовать вашему code-style.
Да-да, если кодовая база однородна и всё такое, то, вроде как, всё хорошо, но как же меня бесит этот camelCase..
Спасибо.
По сути, в плюсах — это аналог rvalue с мув семантикой. Но в плюсах нет поддержки овнершипа на уровне языка/компилятора:
Как я уже писал Halt — смотрю Rust из-за этих манипуляций с лайфтаймом :)
Спасибо за объяснения.
По сути (очень грубо) это и есть тюплы. Просто это strong typedef (т.е., алиас, который кроме синонима вводит ещё и новый тип на основе которого можно делать перегрузку ф-й и прочее. То, чего не хватает плюсам на уровне языка).
struct Meters(f32)— подтверждает это. На псевдо коде мы имеем что-то такого:(Подправте, если ошибаюсь)
С макросами — понял — аналог шаблонов. Но тогда
есть и макросы (которые судя по комментариям ниже могут генерить код/модули) и есть шаблоны.
Спасибо за объяснения. Я фан явного указания жизни обьектов. Из-за вас теперь уже точно смотрю на Rust :)
Спасибо за комментарий.
Предупреждаю — ниже взгляд человека, который НЕ разбирался в языке совершенно ни грамма.
Навскидку:
этот маленький
'крючёчек для меня, как человека с плохим зрением — просто сложно заметен.Непривычно смотрится:
но это просто нужно один раз понять (я не разбирался что всё это значит ещё. Догадываюсь, что, возможно, в Rust-е всё const-by-default, поэтому
&mut. А&— потому что, так же, как и в плюсах, есть возможность передавать по ссылке/по значению. Но опять же — это просто догадки).Непривычно смотрится просто знак восклицания
println!и всякие штуки, типаslice: &[i32].Дальше:
только с комментария догадываюсь, что это аналог алиаса (?):
using Pair = std::tuple<i32, f32>;и, возможно, доступ будет происходить по индексам.Ну, а этот код из статьи — это вообще ад:
Опять же — уверен, что если разобраться — всё становиться логичным.
Спасибо
Я настолько привык к C++, что жить без него не могу и уже не замечаю проблемы с синтаксисом )
Спасибо за советы. Пойду смотреть Rust
Не пытался садится за Rust всерьёз из-за синтаксиса. Он меня жутко пугает. Что посоветуете? Потерпеть сначала, а потом будет всё в порядке?
Не вы одни сдаётесь. Вон, Мейерс тоже сдался. Но это еще не означает, что C++ — уродливый. Вы на новые стандарты C — посмотрите. Там тоже можно за голову взяться. Это нормально для языков с историей, которые еще и развиваются
да, дело вкуса, разници нет. Лично я думаю, что static_cast() — слишком длинно, C-style каст — слишком незаметно, а странный void() — и в глаза падает (идешь гуглить, что это за void()), и не слишком длинный
Раз уж такая пьянка:
unused, хотя бы 1н элементoperator,()для случая, когдаFвозвращает user-defined тип, для которого и определёнoperator,()decay_t<T>вместоtypename decay<T>::type(tuple_size_vможно аналогично заюзать, но уже C++ 17)decay<T>— слишком жёстко, просто —remove_reference<T>Ну и, я думаю, стоило бы упомянуть C++ 17 fold expressions
Да, нужно использовать atomic-операции для свапа: C++ 11 std::atomic_...<std::shared_ptr>
Достойно. Спасибо за ссылку. Получил массу удовольствия :)
Спасибо за ответ.
Я немного не понял вас. Вот моя логика:
sizeof(T)— приведёт к ошибке компиляции, если компилятор не видет определения типаT, т.е.,T— неполный тип.sizeof(T)никак не зависит от того определён для user defined типа конструктор или деструктор. Это означает, чтоsizeof(T) > 0всегда — для нашого случая — нужен только для того, чтобы выдать пользователю ошибку во время компиляции о том, что он забыл определить указанный класс (тип) до места его использования (кстати, посколькуsizeof(T)никогда не может быть 0м — то можно просто писатьsizeof(T)).Дальше: у вас, по сути, таких места 2: конструктор и деструктор. Ставим вопрос — может ли пользователь написать такое использование
PimplPtr<Impl>, чтобыImplбыл, например, определён до вызова конструктораPimplPtr<Impl>и, одновременно, не определён при вызове деструктораPimplPtr<Impl>? (или наоборот). Ответ — да, может:Т.е., да — я вас обманул в деструкторе — для приведённого второго случая — такой
assertбудет полезен, прошу прощения. Но текст сообщения, всё же, немного неправильный.По поводу делегирующего конструктора — я снова вас обманул — имел в виду, что 1й конструктор должен вызывать 2й, т.е., для вашего кода, это что-то типа:
И ещё — поскольку у вас в публичном классе есть функция:
То это означает, что я могу сделать так:
либо, более неявно — через какую-то фабрику:
где
make_T()вернёт нулевой указатель.Т.е. не хватает проверки времени выполнения инварианта
указатель не nullptr— я бы добавилassert()в конструктор и ф-иget().И, моё предвзятое мнение — в мире плюсов — для таких штук как умные указатели — пытаются убежать от неявных опеаторов преобразования: т.е. — пометить бы ещё
operator ElementType()какexplicit...И… и я бы написал
using ElementType = T— меньше кода и проще читается…Извините, я увлёкся — простите за, возможно, нелепую критику — ухожу от клавиатуры :)
Спасибо
Я бы всё-же не приводил "не очень хороший" код в статье для новичков: в первом примере, раз вы явно говорите об деструкторе, то можно бы и явно прописать реализацию конструкторов/операторов присваивания, следуя хорошему тону — The rule of three/five/zero.
auto_ptrя бы выбросил вообще — всё по тому же поводу — deprecated — можно просто ссылочку дать — так для истории и действительно интересующихся. Проunique_ptr, возможно, нужно было бы рассказать, почему деструктор нужно реализовывать в файле-реализации (а что с move-семантикой на этот счёт ?) — но это отклонение от темы — зачем нужна своя реализацияPimplPtr-а?Если убрать из статью вводную, то причина у вас, получается одна — "нарушение логической константности" (а точнее 2ве: "
propagate_constпока не является частью стандарта"). Хорошо, но — код из статьи — не компилируется.По поводу реализации:
static_assert— не нужен — это сделает за вас вызов конструктора вmake_unique.constexpr— зачем? Он сдесь только мешает код читать и нет случая, когда он нужен был бы.explicit(кстати, а почему? почему и не первый ?), наверное, должен вызывать первый (Delegating constructor) — хотя бы для того, чтобы логика в конструкторах совпадалаstatis_assert, когда в конструкторе — она уже есть ?= defaultиспользуется, а в примерах выше, в файле-реализации — нет ?:Widget::~Widget() = defaultget()Спасибо за статью. Извините за, возможно, резкий тон. Хорошего вам дня ^_^
Спасибо.
Не, долго писать, но вкратце:
Если вы будете использовать
std::forward()в этом контексте, то это аналогичноstd::move()для всех параметров. Т.е., с вашим примером, будетstd::move()как для 1го аргумента, так и для второго, т.е., если была бы ещё перегрузкаmoveNamedVector(string&&, vector&&), то вызвалась бы именно она, независимо от того использовали ли бы выstd::move()(более идиоматично) либоstd::forward()(нестандартное использование).По поводу
std::function, да, использование с аллокаторами задепрекейтили в C++17: Deprecating Allocator Support in std::function, потому что:Забавно
Допустим, у нас есть:
parameter в
DoForward(), поскольку это шаблонная функция, попадает в (не знаю как перевести) "deduced context" — и всё что вmain-е я написал, это, как раз таки, "вывод типа" параметра, который учитывает "шаблонный" тип параметра и тип аргумента функции. Из-за того, что это шаблон, как видно, мы можем получить либо rvalue либо lvalue. Всё просто: "deduced context" — юзаемforward().Как работает
forward(): для первого вызова вmain-е, как я уже написал, тип parameter вывелся вUserType&, т.е.,T— этоUserType&.forward()принимает аргументом (всегда!) — именованную переменную — т.е., это всегда lvalue. Получаем, чтоforward()имеет на вход шаблонный аргумент типаUserType&и тип параметра, так же,UserType&— всё это означает, что переданный на вход аргумент — это lvalue!forward()ничего не делает.Аналогично, для второго вызова в
main-е, тип parameter вывелся вUserType&&, т.е.,T— этоUserType&&. Получаем, чтоforward()имеет на вход шаблонный аргумент типаUserType&&и параметрUserType&— переданный на вход аргумент — это rvalue!forward()делаетmove().Во всех остальных случаях — у нас нет "deduced context-а" и всё, каким мы его видим, таким и есть — т.е.
T&&— это rvalue — нужно делатьmove().T&— это ссылка — делаем что хотим, аT— хм, переменная, которая, больше не используется, поэтому можно сделатьmove().Т.е., в случае:
для параметра функции
call()нет вывода типа, потому что он уже выведен для класса в целом (мы его указываем при инстанциировании шаблона:NonDeducedContext<UserType>). Т.е., parameter — это value type — это копия аргумента функцииcall(). Ниже, по коду, он нигде не используется, поэтому я спокойно его муваю, тем самым говоря, что я его больше не использую и "делайте со мной что хотите".Просто хочу заметить, что "если не сильно менять и причёсывать" код из статьи, то, как раз таки,
std::forward()там нигде не нужно, просто потому что форвардинг ссылки нигде и не используются (а должны! К сожалению, иstd::move()упущен… ладно).По поводу вашей реализации, извините, я не сильно всматривался, вам не хватило аллокатора который есть в интерфейсе std::function?
Плюс, по поводу
std::forward(), в этом месте — кхм, навскидку не понял, почему тамstd::forward(), а неstd::move()? Судя по этому месту, я дико извиняюсь, — вы неправльно используетеstd::forward()?Хм, мне кажется, я начинаю завидовать вашему code-style.
Да-да, если кодовая база однородна и всё такое, то, вроде как, всё хорошо, но как же меня бесит этот
camelCase..Как по мне — так тут один решающий недостаток — нельзя написать просто:
std::vector<std::unique_ptr<IA>>new std::vector<T>? Это никак не влияет на скорость/пам'ять?