Структуры вида 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() = default
get()
Спасибо за статью. Извините за, возможно, резкий тон. Хорошего вам дня ^_^
Спасибо.
Не, долго писать, но вкратце:
Если вы будете использовать
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>
? Это никак не влияет на скорость/пам'ять?