OTHER POSTS RSS flux

Templates, Concepts and traits

[2022-05-18] #C++ #Rust #Traits


The common point about these three words is that they all are programming ideas and almost not related to any language feature. Those are the kinds of ideas that are the soul of modern programming.

If templates and concepts are a similar in the sens that it allow the user to do some meta programming, traits are an idiomatic system to (and I quote _Bjarn Stroustrup_ ) carry information used by another object to determine policy or implementation details.

About templates

In C++, templates are very common in the standard library. Even if it tends to be replaced with concepts, you have probably already seen one in your programmer’s life.

It’s all about genericness, if you’re building a library, you probably want that the user could use it with any kind of input. If your library is doing an abstract sequence of operations with the input, there is no reasons to limit the client to a specific Class.

Note that in some cases you precisely want to be specific. For example, when you build a lib with physic’s function, you certainly want to introduce the specification of types like Hz, km. You can so, remotely, introduce some rules of calculations and assignations. That’s one example, but I’m sure there are many other situations in which generic programming tools should not be used.

Unsurprisingly, templates take the form of < T > in many languages. I’ll not go into details of the pattern because I’m sure you can google it if you don’t know what is it.

About concepts

Hmf… 😬 In practice, concepts are very C++ oriented. But it introduces the idea of Constraints that you cannot have with just templates! Basically, it moves the resolution of the template very early in the compilation and gives a flavor to the user to figure out why the type doesn’t work with the library.

In other words:

And the traits

Here is the bridge between Rust and C++ for the meta programming part. In Rust, traits are used to describe both Constraints and implementation details, where in C++, you need to do a mix of all the ideas of that article.

Now, a basic example of an almost word for word translation. You can decline it with any algorithm, nevertheless, I’m going to show how to do the famous ToString traits constraints.

// Declared in the standard library at std::string;
// Let user carry on the implementation for their own structures as `A`
// pub trait ToString {
// 	fn to_string(&self) -> String;
// }

/// Structure defined in the user program
struct A {
  // parameters
};

/// Implementation of the trait `ToString` for local structure `A`
impl ToString for A {
  fn to_string(&self) -> String {
    // implementation
  }
}

In rust, most of traits are already declared in the standard library because it’s very idiomatic to work with. Note that as for a C++ parallel, the implementation for A isn’t inside A. It can, for some reason, being implemented in another file that we would import if we need A to implement the trait!

The library would constrain the used type to implement ToString like that:

pub fn foo(obj: impl ToString) {
  println!("{}", obj.to_string());
}

So what is the C++ equivalent to the paradigm? First you need to declare yourself the trait. Because C++ is not oriented like that and sometime (I don’t understand why) people discourage to do it that way. If you have got an idea please comment the post!

// declare trait in lib.h

template<typename T>
struct trait_to_string {
  // By default, we don't implement that trait for T
  static const bool impl_flag = false;
  static std::string to_string(const T& self);
};

OK, so we are here with a trait_to_string that replace the standard in Rust. And the structure trait_to_string<A> that implement the trait.

You’ve noticed that we already use templates here in C++, and not in Rust. It’s because, behind the hood in Rust we are doing the same thing (but in earlier in the compilation).

Now, we need to constrain the client to use our library with only data that implement our trait ToString-like. We will use… Concepts!!

// still in lib.h

template<typename T>
concept ImplToString = requires(T a) {
  // required to be true
  requires trait_to_string<T>::impl_flag;
};

/**
* Export function log that is very simple...
*/
template <typename T>
requires ImplToString<T>
std::string log(T a)
{
  return trait_to_string<T>::to_string(a);
}

And we expose a basic function log that don’t even do anything so it’s a bit miss-named. Whatever, you’re not forced to use the concepts here, you can do the same thing with a simple <Template> and documentation! In addition, you shouldn’t use Concepts only to do that kind of stuff. It will take a full dedicated article about that subject.

Then, once you’re done with your library, you can use it that way:

struct A {
  A(std::string a): str(a) {};
  std::string str;
};

template<>
struct trait_to_string<A> {
  static const bool impl = false;
  static std::string to_string(const A& self) {
    return self.str;
  }
};

int main() {
  A a("hello world");
  std::cout << log(a);
}

I’m agree, the flag isn’t really beautiful, it allow us to put a Concept constraint and we’ll probably get a better way to define that. To be honest, there is probably already a solution that I don’t know. :-D

However, look at the definition of the trait… Isn’t it Rusty?!

I leave you here for the moment! I’ll be back in next articles to be more precise about Concepts. I promise to look attentively at that flag too.

Conclusion

Rust compiler do a lot of things for us to make the pattern efficient and easy to use. In addition, there is a little thing that we learn the last 20 years about hat pattern called the Orphan rule, and it’s probably the reason why I didn’t heard much about traits in C++.

I really want to test the limits in C++ of the pattern! I’ll be back!


Comments

Join the discussion for the article on this ticket.