Dec 31, 2023

[Cppcon 2023] Expressing Implementation Sameness and Similarity - Polymorphism in Modern C++, Daisy Hollman

Reference:
https://youtu.be/Fhw43xofyfo?si=PcMFEsqb7NRiCmyX

Two different kinds of sameness

Interface sameness

  • Enables users to treat things the same way in their code(create their own sameness)
  • Cannot be removed later
    • You can't change your user's code(at least not easily)
    • Once you allow users to treat things as the same, it's very hard to change that later
  • When done well: low code coupling(the degree of interdependence between software modules)

Implementation sameness

  • Enables readers to understand and use existing sameness of similarity
  • Can be changed or removed at any point in the future when it stops being helpful
  • When done well: high code cohesion(the degree to which the elements inside a module belong together)
Let this concept sinks in.

Consider about 'type of T'; e.g. template<typename T> vector{};


Reusing customization points.

Mixin mixins.

template<class> struct CRTPMixin;
template<template <class> class Mixin, class Derived>
struct CRTPMixin<Mixin<Derived>> {
  consteval auto& self() { return static_cast<Derived&>(*this); }
  consteval auto const& self() const { return static_cast<Derived const&>(*this); }
};

template<typename Derived>
struct PrintableElementwise : CRTPMixin<PrintableElementwise<Derived>> {
 void print() const {
    auto const& e = self().elements();
    std::apply([](auto const&... el) {
      ([&]{ cout << el << "\n"; }(), ...);
      }, e);
    }
};


As stated by Sy Brand's C++23’s Deducing this can help with the above

https://devblogs.microsoft.com/cppblog/cpp23-deducing-this/
static_cast<Derived const&>(*this); 
part. But really? Consider code uses deduce this from C++23:
struct PrintableElementwise {
  void print(this auto const& self) {
    auto const& e = self.elements();
    std::apply([](auto const&... el) {
      ([&]{ count << el << "\n"; }(), ...);
    }, e);
  }
};

struct Foo : PrintableElementwise {
 int a;
 double b;
 std::string s;
 auto elements() const {
    return std::forward_as_tuple(x, y, z);
 }
};
Above has mixed interface sameness with implementation sameness; which causes error:
auto items = std::vector<PrintableElementwise>{/*...*/};
for (auto* i : items) {
    i->print(); // error if derived type has no memfn elements().
}
Use the idea of 'type of T'. Which indicates the type's embedded behavior.


Qualifier forwarding

e.g.
Perfect forwarding:
template<class T>
void foo(T&& t) {
    bar(std::forward<T>(t));
}
equivalent to:
template<class T>
void foo(T& t) {
 bar(t);
}

template<class T>
using not_forwarding_ref = T;

template<class T>
void foo(not_forwarding_ref<T>&& t) {
  bar(std::move(t));
}


Don't Repeat Yourself(DRY)

'Every piece of knowledge must have a single, unambiguous, authoritative representation within a system


Separating the ownership mechanism (typical pattern for a class template customization point

From:

template<class Thing>
class OwingCollection {
 private:
   vector<unique_ptr<Thing>> things_;
 protected:
   void for_each(/* concept */ std::invocable<Thing const&> auto&& f) const { /*...*/};
 public:
   void insert(unique_ptr<Thing>) {};
   unique_ptr<Thing> remove(unique_ptr<Thing>) {};
   bool contains(unique_ptr<Thing>) const {};
   void remove_if(invocable<Thing const&> auto&&) {};
};
To:
template<class Thing, template<class...> class Owner = unique_ptr>
class OwingCollection {
 private:
   vector<Owner<Thing>> things_;
 protected:
   void for_each(/* concept */ std::invocable<Thing const&> auto&& f) const { /*...*/};
 public:
   void insert(Owner <Thing>) {};
   Owner <Thing> remove(Owner <Thing>) {};
   bool contains(Owner <Thing>) const {};
   void remove_if(invocable<Thing const&> auto&&) {};
};


STD's example of separable pattern 

[C++14] unique_ptr with type erasure as shared_ptr 


C++20 Concepts

C++20 concepts extract interface sameness without your permission.

Concepts allow library users to accidentally create code coupling between unrelated modules based only on names.
e.g.
template<class T>
  requires requires(T&& t) { { t.clear() } -> convertible_to<bool>; }
void do_the_stuff(T&& t) { /*...*/ }
Those two types fit for above API. 
However; their meaning of memfn clear() is different.
struct Container {
  // returns true if the container was
  // non-empty before the clear
  bool clear();
};

struct Color {
  // returns true if opacity == 0
  bool clear();
};
Concepts does not differentiate namespace.


More places having the concept of 'sameness'

  • 'Normal' functions (C like)
  • Macros and Code generation
    • 'Gross' but sometimes better than repeating things.
  • Customization Point Objects(CPOs)
  • Type erasure
  • constexpr functions
    • 'Sameness' of compile-time and runtime implementations.
  • Dependency injection (needs reflection)
  • Aspect-oriented programming(needs reflection)
  • Decoration(needs reflection)

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.