3

So, I'm struggling to grasp the whole CRTP thing in C++. A few (1) (2) of the rationales I found online about why should you even care to use it is that it allows to add a functionality to your code in a semi automatic way. What I mean is that I can let the compiler deduce some functionality for me, asking me only for a few small implementation.

As an example, I may want to formalize a Comparable "trait" (using a Rust word), forcing the user to provide an implementation for < and ==, and deducing all other comparisons using just these two. IIRC, this is what happens when you instantiate a std::set of a custom class (the compiler asks you for a operator<() implementation).

So, I have two question about C++'s CRTP:

  1. In this sense, is the usage of the word "trait" appropriate? And may I even go further and say abstract data type? Is CRTP the C++ idiom to achieve this (C++ purists, forgive me)? Because to me they seem very similar: you define some basic implementation, and following some deduction rules you can get other behavior/functionality
  2. Can I get the same thing ("trait"-like thing) by using an abstract class? If so, why should I use a CRTP instead?

Thanks!


EDIT: in the comments I have been advised that in C++ the word "trait" refers to a particular concept, which is different to the one used for instance in Rust. So I slightly change/clarify my question: is CRTP similar to Rust's traits (IIUC, specifically the #[derive]d ones)?

  • 2
    What does "ADT" mean? – Nicol Bolas Dec 13 '22 at 00:03
  • Abstract data types or algebraic data types, the question may address both but I reckon the former is probably more accurate. I'll edit the question anyway, thanks – Alessandro Bertulli Dec 13 '22 at 00:07
  • 2
    I suggest you to look at concrete examples of CRPT, before trying to draw parallels. – 463035818_is_not_an_ai Dec 13 '22 at 00:08
  • 1
    Btw containers that use <, typically use only that for equivalence, which is not equality, ie they do not "deduce" ==. – 463035818_is_not_an_ai Dec 13 '22 at 00:12
  • 3
    In simple words, CRTP is compile-time polymorphism. Its main advantage is that a compiler can potentially inline most of the code in contrast to run-time virtual calls, which can be devirtualized and inlined only in some special cases. If you take a look at some linear algebra libraries like Eigen or MTL, they are typically implemented with a heavy usage of CRTP technique. – Evg Dec 13 '22 at 01:48
  • 1
    "Trait" in C++ is a specific thing, different from CRTP. – Eugene Dec 13 '22 at 03:33
  • 2
    C++ doesn't have traits like rust does. I suppose the closest thing to a rust trait is a C++20 concept, where you formulate requirements on a type e.g. a type must be copy-constructible or have this or that member function. In C++ "generics" is achieved through templates. You can write function or class templates accepting any type satisfying certain concept(s). – joergbrech Dec 13 '22 at 04:21
  • 1
    an abstract class is about runtime polymorphism, crpt not. Note that if `foo` inherits from `crtp` and `bar` inherits from `crtp` then they do not have a common base. – 463035818_is_not_an_ai Dec 13 '22 at 08:51
  • Thanks, to all! But I still have a doubt, which I'll add in the question, if you like to answer – Alessandro Bertulli Dec 13 '22 at 20:29

2 Answers2

2

The CRTP idiom is a tool that injects generic functionality into a class, specifically by inserting members into it. The general, language-neutral, term for this kind of thing is a "mixin." Mechanically, "mixin" usually refers to ways of injecting members into a class without using a base class. Since the CRTP uses base classes, it does not strictly fit the definition of a mixin. But this is a matter of mechanism, not concept; a CRTP base class fulfills the general function of a mixin even if not by the usual means.

Rust traits are kind of like mixins as well. But the CRTP is not like a Rust trait for one very important reason. The whole point of Rust traits is that the class on the receiving end does not have to know that they are being given a trait. In Rust, you can force a type to have a trait interface without that type having any idea that the trait even exists.

The CRTP can't do that. A class must choose to opt-into a CRTP-based mixin interface.

So no, I would not call the CRTP a Rust trait. Indeed, the C++ concept you found called "traits" are much more like Rust traits. C++ traits define an interface that any type could adopt, and via template specialization, a user can adapt its normal interface to the traits interface. Same idea, just via a different mechanism.

The nominal similarity is not a coincidence: the makers of Rust recognized the utility of the C++ traits idiom and built a language feature specifically to facilitate it (and thus dodging all of the cruft that comes from using traits-based interfaces in C++).

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 1
    Since [`#[derive]`](https://doc.rust-lang.org/reference/attributes/derive.html) was explicitly mentioned in the question but not this answer: IIUC this feature automatically implements a trait for you. I don't believe that anything like it exists in C++, because to implement it, you need to be able to inspect a types members at compile time. This is possible in rust, because you have access to rust's AST through procedural macros. For it to work in C++, you would at least need compile-time reflection. – joergbrech Dec 14 '22 at 08:14
  • Thanks! I have a question about the last part of the answer: IIUC, C++ traits needs you to specify (via template specification) what type are you operating on. In Rust, this is not the case, so much so that you can actually use traits like OO `interface`s. Is this another quite relevant difference, or did I misunderstood something? – Alessandro Bertulli Dec 14 '22 at 12:24
  • 1
    @AlessandroBertulli: "*C++ traits needs you to specify (via template specification) what type are you operating on.*" Who is "you" in this scenario? Is it the writer of the type or the code using a trait to interact with that type? If it's the former, no, they don't need to know it, depending on how the trait works and searches for functionality in the source type. If it's the latter, then they are usually templates who only know it by a template parameter. Rust just makes that invisible. – Nicol Bolas Dec 14 '22 at 14:27
  • @NicolBolas Thanks, I think I'm starting to see what I couldn't grasp. Since I'm not very practical of C++ trait use cases, do you have some example/resources? The [ones I found](https://www.internalpointers.com/post/quick-primer-type-traits-modern-cpp) are limited to implementing things like numeric limits, or anyway they focus on [providing info about a type](https://en.cppreference.com/w/cpp/header/type_traits), not actually implementing methods/adding functionalities. May [this question](https://stackoverflow.com/q/66818748/17789881) be related? – Alessandro Bertulli Dec 14 '22 at 14:46
  • 1
    @AlessandroBertulli: "*do you have some example/resources*" `iterator_traits` is why a pointer can be a iterator. But traits are primarily a means of exposing/manufacturing metadata information (hence the word "trait"); `allocator_traits` is the big one that adds function calls. The more common C++ mechanism for extending a class interface is to use free functions like `std::begin` or the more modern customization point object idiom like `std::ranges::begin`. Rust traits were built to overcome the limitations with C++ traits and make them more useable for extending a class's interface. – Nicol Bolas Dec 14 '22 at 14:54
  • Thanks, I need to further investigate this (currently I'm not sure I know enough the subject to appreciate it, and I don't want to go off topic). Anyway you helped me a lot, if I'll need further assistance I'll open a question specifically on the historical difference of Rust traits over C++'s ones. Kudos! – Alessandro Bertulli Dec 14 '22 at 16:03
1

In addition to @Nicol Bolas excellent answer I would like to add some comments on #[derive] traits. In rust, you can automatically implement some traits for your structs using this keyword. Here is an example taken from the link above:

#[derive(PartialEq)]
struct Foo<T> {
    a: i32,
    b: T,
}

The equivalent in C++ would be an automatic mixin of code, such that Foo fufills the std::equality_comparable concept. This is currently impossible because an automatic implementation would require a built-in reflection system, which C++ does not have: We would have to inject an equality operator, that checks all data members for equality, so we need to know about all data members of the type we are injecting the functionality into. There is a current proposal for built-in reflection, but even with it, it would probably be impossible to write a CRTP base class PartialEq that implements a meaningful bool operator==(X const& other) const noexcept for you. Consider the following example:

// hypothetical crtp mixin
template <typename T>
struct Foo : public PartialEq<Foo<T>> {
    int a;
    T b;
};

PartialEq<Foo<T>> needs to know about all datamembers of Foo<T>, but at time of the definition of PartialEq, Foo<T> is incomplete: It must be defined afterwards, because it depends on PartialEq through inheritence.

But there is yet another proposal on facilitating metaprogramming in C++ that introduces features somewhat similar to procedural macros in rust. If it gets accepted and implemented, it will be added no earlier than 2026. This proposal includes something called a metaclass, which pretty much is very close to a #[derive] trait in rust - without CRTP. The syntax would look like this:

template <typename T>
struct Foo(PartialEq) {
    int a;
    T b;
};

This usage scenario is even explicitly mentioned in the proposal paper, though there is a talk of a regular interface (a supertrait, in rust-speak), that injects code to satisfy the equality_comparable concept together with some other code.

joergbrech
  • 2,056
  • 1
  • 5
  • 17