Lazy type constraint (implement Rust `Default` trait in c++)

199 Views Asked by At

In Rust you can implement traits for structs and each implementation has own type constraint. (for who may does not familiar with rust, you can consider a "trait" as a "base class" and "implementation" as "inheritance"). look at this example:

// our struct has an item of type mutable T pointer
struct Box<T> {
    inner: mut* T
} 

// implementing `Default` trait for 
// box. in this implementation, type 
// constraint is: `T: Default`. 
// it means the inner type also must 
// implements Default. 
impl <T: Default> for Box<T> {
    fn default() -> Self {
        return Box::new(T::default());
    }
}

the note in this example is there is no need T: Default to be applied until you use Box::default() in your code.

well it is possible to do like this in cpp? I'm familiar with how we can constraint types in cpp and this is not my problem. I want to know is there some way to lazy type constrain (maybe a better description) in cpp when I define a class?

I know it is possible with if constexpr or static_assert. but this way is not beautiful. I want a template or concept solution ( I mean what applies to function signature in fact) if possible. thank you for any guidance.

3

There are 3 best solutions below

0
user17732522 On BEST ANSWER

I am not sure I follow exactly what you want (I don't really know Rust), but you can constrain member functions individually:

template<typename T>
concept Default = requires {
    { T::default_() } -> std::same_as<T>;
};

template<typename T>
struct Box {
    static Box default_()
    requires Default<T> {
        //...
    }
    //...
};

Now Box itself has no requirements on T, but to use Box::default() will require T to satisfy the Default concept.

However, it would basically work without the constraint as well. If Box<T>::default calls T::default() and the latter is not well-formed the code will fail to compile if and only if Box<T>::default is actually used in a way that requires it to be defined (i.e. called or pointer/reference to it taken). But without the requires clause e.g. Default<Box<T>> would always report true.

And of course in C++ we would use the default constructor instead of a static member function called default to construct the object. The same approach applies to constructors though and there is already the concept std::default_initializable for that in the standard library.

As far as I understand, Rust traits do not really overlap fully with either C++ (abstract) base classes nor concepts though. The approach above will not allow for runtime polymorphism on types satisfying Default. For that an abstract base class with the interface functions as non-static pure virtual member functions should be used instead.

However, the trait Default only imposes a requirement on a static member function, so that it shouldn't be relevant to runtime polymorphism, but only compile-time properties of the type, which is what concepts are for, although in contrast to Rust you don't declare a type to implement a trait. Instead the concept describes requirements which a type needs to satisfy to satisfy the concept.

0
Quimby On

Yes, it can be done with concepts:

#include <concepts>

template<std::default_initializable  T>
struct Box {};

struct Yes{};

struct No{ No(int){}}; // No default ctor
int main()
{
    Box<Yes> y;
    Box<No> n;
}

which outputs:

<source>:14:11: error: template constraint failure for 'template<class T>  requires  default_initializable<T> struct Box'
   14 |     Box<No> n;
      |           ^
<source>:14:11: note: constraints not satisfied
In file included from <source>:1:
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/concepts: In substitution of 'template<class T>  requires  default_initializable<T> struct Box [with T = No]':
<source>:14:11:   required from here
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/concepts:136:13:   required for the satisfaction of 'constructible_from<_Tp>' [with _Tp = No]
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/concepts:141:13:   required for the satisfaction of 'default_initializable<T>' [with T = No]
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/concepts:137:30: note: the expression 'is_constructible_v<_Tp, _Args ...> [with _Tp = No; _Args = {}]' evaluated to 'false'
  137 |       = destructible<_Tp> && is_constructible_v<_Tp, _Args...>;
      |                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Box<Yes> will compile without issues. The above is a shorthand for the following which allows to chain more requirements

template<typename  T>
requires std::default_initializable<T>
struct Box { };
0
Redwan On
#include <type_traits>
#include <concepts>

class SomeBaseClass {};

template<typename T>
concept Derived = std::is_base_of_v<SomeBaseClass, T>;

template<typename T>
concept Default = requires(T) {
    { T::by_default() } -> std::convertible_to<T>;
};

template<Derived T>
class Box {
    T* inner;

    Box(T data): (&data);

  public:
    static Box by_default() 
    requires(Default<T>) {  // This was my lost!
        return Box(T::by_default());
    }
};


class Object: SomeBaseClass {
  public:
    static Object by_default() {
        return Object();
    }
};


int main() {
    Box<Object> b = Box<Object>::by_default();
}