What are the similarities and differences between C++'s concepts and Rust's traits?

C++RustTraitsC++ Concepts

C++ Problem Overview


In Rust, the main tool for abstraction are traits. In C++, there are two tools for abstractions: abstract classes and templates. To get rid of some of the disadvantages of using templates (e.g. hard to read error messages), C++ introduced concepts which are "named sets of requirements".

Both features seem to be fairly similar:

  • Defining a trait/concept is done by listing requirements.
  • Both can be used to bound/restrict generic/template type parameters.
  • Rust traits and C++ templates with concepts are both monomorphized (I know Rust traits can also be used with dynamic dispatch, but that's a different story).

But from what I understand, there are also notable differences. For example, C++'s concepts seem to define a set of expressions that have to be valid instead of listing function signatures. But there is a lot of different and confusing information out there (maybe because concepts only land in C++20?). That's why I'd like to know: what exactly are the differences between and the similarities of C++'s concepts and Rust's traits?

Are there features that are only offered by either concepts or traits? E.g. what about Rust's associated types and consts? Or bounding a type by multiple traits/concepts?

C++ Solutions


Solution 1 - C++

Disclaimer: I have not yet used concepts, all I know about them was gleaned from the various proposals and cppreference, so take this answer with a grain of salt.

Run-Time Polymorphism

Rust Traits are used both for Compile-Time Polymorphism and, sometimes, Run-Time Polymorphism; Concepts are only about Compile-Time Polymorphism.

Structural vs Nominal.

The greatest difference between Concepts and Traits is that Concepts use structural typing whereas Traits use nominal typing:

  • In C++ a type never explicitly satisfies a Concept; it may "accidentally" satisfy it if it happens to satisfy all requirements.
  • In Rust a specific syntactic construct impl Trait for Type is used to explicitly indicates that a type implements a Trait.

There are a number of consequences; in general Nominal Typing is better from a maintainability point of view -- adding a requirement to a Trait -- whereas Structural Typing is better a bridging 3rd party libraries -- a type from library A can satisfy a Concept from library B without them being aware of each other.

Constraints

Traits are mandatory:

  • No method can be called on a variable of a generic type without this type being required to implement a trait providing the method.

Concepts are entirely optional:

  • A method can be called on a variable of a generic type without this type being required to satisfy any Concept, or being constrained in any way.
  • A method can be called on a variable of a generic type satisfying a Concept (or several) without that method being specified by any Concept or Constraint.
  • Constraints (see note) can be entirely ad-hoc, and specify requirements without using a named Concept; and once again, they are entirely optional.

Note: a Constraint is introduced by a requires clause and specifies either ad-hoc requirements or requirements based on Concepts.

Requirements

The set of expressible requirements is different:

  • Concepts/Constraints work by substitution, so allow the whole breadth of the languages; requirements include: nested types/constants/variables, methods, fields, ability to be used as an argument of another function/method, ability to used as a generic argument of another type, and combinations thereof.
  • Traits, by contrast, only allow a small set of requirements: associated types/constants, and methods.
Overload Selection

Rust has no concept of ad-hoc overloading, overloading only occurs by Traits and specialization is not possible yet.

C++ Constraints can be used to "order" overloads from least specific to most specific, so the compiler can automatically select the most specific overload for which requirements are satisfied.

Note: prior to this, either SFINAE or tag-dispatching would be used in C++ to achieve the selection; calisthenics were required to work with open-ended overload sets.

Disjunction

How to use this feature is not quite clear to me yet.

The requirement mechanisms in Rust are purely additive (conjunctions, aka &&), in contrast, in C++ requires clauses can contain disjunctions (aka ||).

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionLukas KalbertodtView Question on Stackoverflow
Solution 1 - C++Matthieu M.View Answer on Stackoverflow