Skip to content

adamuso/simple_class_traits

Repository files navigation

Simple Class Traits

Inspired by Rust Traits I've recreated this mechanism in C++ using templates and specializations.

Work in progres...

TODO:

  • Defining traits as simple class
  • Basic trait implementation
  • Generic trait implementation
  • Allow implementing traits for other traits
  • C++11 compatible
  • Generic trait definition
  • Add a namespace for traits
  • No std library dependency on core implementation
  • Have a way of handling associated types (although its a bit trickier than in rust)
  • Simplify trait implementation declaration
  • Make it simple
  • Make it 100% safe
  • Handle cases like std::vector<Trait::ref>
  • Somehow allow using traits with shared_ptr

Usage

Defining a trait

To create a new trait you need to define a class that have virtual method.

class DebugPrint
{
public:
    virtual void print() const = 0;
};
// Comparison to rust
trait DebugPrint {
    fn print(&self);
}

Additionally you can add ref typedef for convinience. It can make easier accessing the trait reference type.

class DebugPrint
{
public:
    typedef trait::ref<DebugPrint> ref;

    virtual void print() const = 0;
};

// Defining trait as parameter in function
// without `ref` typedef:
void example(trait::ref<DebugPrint> dp);

// with `ref` typedef
void example2(DebugPrint::ref dp);

Traits can be also defined as templates.

template <typename T>
class GenericTrait
{
public:
    virtual void do_something(T v) const = 0;
};
// Comparison to rust
trait GenericTrait<T> {
    fn do_something(&self, v: T);
}

Traits can provide defualt implementation.

class SomeTrait
{
public:
    virtual int get_a() const = 0;
    virtual int get_b() const = 0;

    virtual int sum() {
        return get_a() + get_b();
    }
};

We can also simulate Output type for a function.

// How its done in rust
trait Add<T> {
    // Here Output is not a part of template
    type Output;

    fn add(&self, other: T) -> Self::Output;
}
// Output in C++ needs to be a part of template, otherwise we cannot achieve same effect as in rust
template <typename T, typename Output = void>
class Add
{
public:
    // Because `Output` is a part of template, it should not be used for implementation
    // that is why a special `tag` typedef is added below, it basically says that for an
    // trait implementation we should only look at Add<T> without `Output` type argument 
    typedef Add<T> tag;

    // also ref must be specified with all template arguments, it is similiar to rust 
    // that if `SomeTrait` has associated type Output then `dyn SomeTrait` must specify
    // all associated types, example: `dyn SomeTrait<Output = i32>`
    template <typename _Output>
    using ref = trait::ref<Add<T, _Output>>;

    virtual Output add(T other) const = 0;
};

Note: I think this implementation can be improved, I would like to pass as Self type to the trait so you can write Self::Output like in rust

Implementing a trait

Creating a trait implmentation in C++ esessntially means that you need to create a specialization for trait::impl template and provide body for virtual methods. To make this process easier there are few optional #define which can make the code more readable. If you don't like using defines then all traits can be implemented in standard C++ way.

// Implementation using define is very simple
trait_impl(DebugPrint, int)
{
public:
    void print() const 
    {
        std::cout << *self;
    }
};
// This is how it looks without macro
template<>
class trait::impl<DebugPrint, int> : public trait::container<DebugPrint, int>
{
public:
    void print() const 
    {
        std::cout << *self;
    }
};
// Comparison to rust
impl DebugPrint for i32 {
    fn print(&self) {
        print!("{}", self);
    }
}

A trait implementation can aslo be generic.

// Using a macro
trait_impl_gen((typename T), GenericTrait<T>, T)
{
public:
    void do_something(T v) const 
    {
       // ...
    }
};
// Without a macro
template<typename T>
class impl::trait<GenericTrait<T>, T> : public trait::container<GenericTrait<T>, T>
{
public:
    void do_something(T v) const 
    {
       // ...
    }
};
// Comparison to rust
impl<T> GenericTrait<T> for T {
    fn do_something(&self, v: T) {
        // ...
    }
}

You can implement a trait for another trait.

// Using a macro
trait_impl_ext(DebugPrint, SomeTrait)
{
public:
    void print() const 
    {
        // this->self is SomeTrait*
    }
};
// Without a macro
template<typename V>
class impl::trait<DebugPrint, V, trait::has_trait<V, SomeTrait>> : public trait::container<DebugPrint, V>
{
public:
    void print() const 
    {
        // this->self is SomeTrait*
    }
};
// Comparison to rust
impl<V> DebugPrint for V 
    where V : SomeTrait {
    fn print(&self) {
        // ...
    }
}

Using a trait reference and pointer

To invoke trait methods you can use two classes: trait::ptr and trait::ref. trait::ptr should be treated as C++ pointer, it won't automatically free memory, it just holds in the inside a pointer to an object and correct trait. All object pointers implementing trait T can be assigned to trait::ptr<T>. trait::ref should be used as C++ reference, it cannot hold nullptr value, it can be assigned only once and it cannot be dereferenced. All references to objects implementating trait T can be assigned to trait::ref<T>. Also trait::ref<T> is implicitly convertible to T& and operator & is overloaded so it returns trait::ptr<T>. Access to methods on both is done using -> operator (I would like . operator for trait::ref but unfortunately this is not possible).

trait::ref corresponds to dyn Trait syntax in Rust.

void do_print(const trait::ref<DebugPrint> dp)
{
    dp->print();
}
// Comparison to rust
fn do_print(dp: &dyn DebugPrint)
{
    dp.print();
}

To achieve impl Trait we can use a template and trait::impl_ref type.

template<typename V>
void do_print(const V& v)
{
    trait::impl_ref<DebugPrint, V> dp = v;
    dp.print();
}
// Comparison to rust
fn do_print(dp: impl DebugPrint)
{
    dp.print();
}

Example

Here are some examples with Rust equivalent. See more examples in example directory.

Example 1:

trait DebugPrint {
    fn print(&self);
}

impl DebugPrint for i32 {
    fn print(&self) {
        println!("{}", self);
    }
}

fn debug_print(p: impl DebugPrint) {
    p.print();
}

fn main() {
    let a: i32 = 2;
    debug_print(a);
}
// example/simplest_print.cpp

#include <iostream>

#include "../simple_class_traits.hpp"

class DebugPrint 
{
public:
    typedef trait::ref<DebugPrint> ref;

    virtual void print() const = 0;
};

trait_impl(DebugPrint, int)
{
public:
    void print() const 
    {
        std::cout << *self;
    }
};

void debug_print(const DebugPrint::ref p)
{
    p->print();
}

int main()
{
    debug_print(2);
    return 0;
}

Example 2:

trait SizedIntArray {
    fn size(&self) -> usize;
    fn item(&self, index: usize) -> i32;
}

trait DebugPrint {
    fn print(&self);
}

impl DebugPrint for i32 {
    fn print(&self) {
        print!("{}", self);
    }
}

impl<T> DebugPrint for T 
    where T: SizedIntArray {
    fn print(&self) {
        for i in 0..self.size() {
            self.item(i).print();
            print!(", ");
        }
        
        println!();
    }
}

impl SizedIntArray for Vec<i32> {
    fn size(&self) -> usize {
        self.len()
    }
    
    fn item(&self, index: usize) -> i32 {
        self[index]
    }
}

fn main() {
    let a: Vec<i32> = vec![1, 2, 3];

    a.print();
}
// example/simple_print.cpp

#include <iostream>
#include <vector>

#include "../simple_class_traits.hpp"

class SizedIntArray
{
public:
    typedef trait::ref<SizedIntArray> ref;

    virtual int size() = 0;
    virtual int item(int index) = 0;
};

class DebugPrint 
{
public:
    typedef trait::ref<DebugPrint> ref;

    virtual void print() = 0;
};

trait_impl(DebugPrint, int)
{
public:
    void print()
    {
        std::cout << *self;
    }
};

trait_impl(DebugPrint, SizedIntArray::ref)
{
public:
    void print()
    {
        for (int i = 0; i < self->size(); i++)
        {
            DebugPrint::ref(self->item(i))->print();
            std::cout << ", ";
        }

        std::cout << std::endl;
    }
};

trait_impl(SizedIntArray, std::vector<int>)
{
    int size()
    {
        return self->size();
    }

    int item(int index) 
    {
        return (*self)[index];
    }
};

int main()
{
    std::vector<int> a = { 1, 2, 3 };
    DebugPrint::ref(SizedIntArray::ref(a))->print();

    // DebugPrint::ref(a)->print(); - doesn't working without an additional template, see simple_print_generic.cpp 
    return 0;
}

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors