From cc5ca25fa54bdea11c3f8714cdedb6938ad77c7c Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 29 Aug 2020 22:35:25 -0400 Subject: [PATCH] Method qualifiers are weirder than I thought. --- _posts/2020-08-05-static-polymorphism.md | 101 ++++++++++++++++++++--- 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 739c73d..b1ad58f 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -115,7 +115,7 @@ public: Rust allows declaring immutable, mutable, and consumed arguments (including `self`). -C++ can use `const_cast` to assert "constness" of `this` and method arguments: +C++ can use `const_cast` to assert "constness" of `this`: ```c++ #include @@ -124,7 +124,6 @@ C++ can use `const_cast` to assert "constness" of `this` and method arguments: template concept ConstMethod = requires (T a) { { const_cast(a).method() } -> std::same_as; - { a.another(std::declval()) } -> std::same_as; }; std::uint64_t my_function(ConstMethod auto a) { @@ -136,11 +135,6 @@ public: std::uint64_t method() const { return 42; } - - // NOTE: non-`const` value is also acceptable here. - std::uint64_t another(const std::uint64_t value) { - return value; - } }; class WithoutConst { @@ -148,10 +142,6 @@ public: std::uint64_t method() { return 42; } - - std::uint64_t another(const std::uint64_t value) { - return value; - } }; int main() { @@ -184,8 +174,91 @@ int main() { | ^~~~~~ ``` -...but difficult to do anything beyond that. Is there a way to declare methods must be `noexcept`, -`volatile`, etc.? Also can't have methods that consume `this`. +...but can't mark `this` as consumed. + +Working with `const` parameters can be a bit weird because of implicit copies: + +```c++ +#include +#include + +class WithCopyCtor { +public: + WithCopyCtor(const WithCopyCtor &other) = default; +}; + +class WithoutCopyCtor { +public: + WithoutCopyCtor(const WithoutCopyCtor &other) = delete; +}; + +template +concept ConstArgument = requires (T a) { + // Arguments passed by value: + { a.method_one(std::declval()) } -> std::same_as; + { a.method_two(std::declval()) } -> std::same_as; + + // Arguments passed by reference: + { a.method_three(std::declval()) } -> std::same_as; + { a.method_four(std::declval()) } -> std::same_as; + + // NOTE: This requirement is illogical. It's impossible to call a method accepting a parameter + // by value when that parameter can not copy construct. + // Not sure if it's worth including this note in the final write-up though. + //{ a.method_four(std::declval()) } -> std::same_as; + + { a.method_five(std::declval()) } -> std::same_as; +}; + +std::uint64_t my_function(ConstArgument auto a) { + return 42; +} + +class MyClass { +public: + // NOTE: Even though the concept required `method_one` to accept `const std::uint64_t`, we don't need + // to use a `const` qualifier here because we can implicitly copy `const std::uint64_t` to `std::uint64_t`. + std::uint64_t method_one(std::uint64_t value) { + return 42; + } + + // NOTE: Similar to `method_one`, even though the concept declared `const WithCopyCtor`, + // we can use the copy constructor to implicitly copy and convert between `const` and non-`const`. + std::uint64_t method_two(WithCopyCtor value) { + return 42; + } + + // NOTE: Because we can't implicitly copy from `const` references to non-`const` references, + // _even if the class has a copy constructor_, we must include the qualifier here. + std::uint64_t method_three(const WithCopyCtor &value) { + return 42; + } + + // NOTE: Similar to `method_three`, because we can't copy from `const` rvalue references to non-`const`, + // we must include the qualifier. + std::uint64_t method_four(const WithoutCopyCtor &&value) { + return 42; + } + + // NOTE: We can _add_ a `const` qualifier even if the concept doesn't require it, because it's safe to + // treat non-`const` references as `const. + std::uint64_t method_five(const WithoutCopyCtor &value) { + return 42; + } +}; + +int main() { + auto x = MyClass{}; + my_function(x); +} +``` + +Rust is much simpler about all this - the signature for a trait implementation must _exactly_ match +a trait definition. + +C++ also has way more qualifiers - `noexcept`, `override`, `volatile`, but I can't find a way to +require those qualifiers being present. In contrast Rust doesn't have exceptions, doesn't have +inheritance, and uses `unsafe` to handle `volatile`, so doesn't need to care about these qualifiers. # Implement methods on remote types @@ -341,6 +414,8 @@ C++ concepts are purely duck typing. ## Move/consume `self` as opposed to `&self`? +Handled as part of method qualifiers. + Not exactly polymorphism, but is a significant feature of Rust trait system. Is there a way to force `std::move(object).method()`? C++ can still use objects after movement makes them invalid, so not sure that it makes conceptual sense - it's your job to prevent use-after-move, not the compiler's.