Defining interfaces in C++ with ‘concepts’ (C++20)
lemire.me
lemire.me
Emphasis mine. While it is somewhat good as documentation, sometimes static_assert with a custom diagnostic message is sometimes better for this purpose.
The hidden power of concepts/constraints is the way it shapes overload sets. You can have something like:
template <forward_range R>
void foo(R&& r) {
/* some generic algorithm */
}
template <random_access_range R>
void foo(R&& r) {
/* optimized for random access */
}
and it will work if you pass a random access range, as the compiler can deduce that it's more specific than a forward range to resolve the ambiguity. Prior to concepts writing code that did this was way more cumbersome.The shorter template syntax is a bonus.
edit: concepts should also allow for better IDE tooling, for example proper completion inside template functions; although it is supposed to work, I didn't notice it firing yet in clangd.
The Go version that was presented isn't equivalent though. In Go you are accepting an interface directly which will hide the value under some fat pointer for dynamic dispatch, in c++ you are using generics to monomorphise the function to specific types. If you want to compare the implementations fairly you should've used Go generics:
func Count[T IntIterable](i T) (count int) {Some reading: https://github.com/golang/proposal/blob/master/design/generi... https://planetscale.com/blog/generics-can-make-your-go-code-...
Considering the progress Go compiler has gone through, I think it's reasonable to expect the optimized implementations will come few versions down the road.
[1] https://archive.org/details/advancedcbsprogr00copl "Advanced C++ Programming Styles and Idioms" aka the first programming book that genuinely kicked my ass when I first read it and made me realise how good it was possible to be at computer science.
extremely hard is underselling it somewhat :)
[1] which has always been strong even before there was an actual ISO/ANSI standard
It is a good intro overview to the subject. Unfortunately the video has far too much filler, so you need to skip a bit e.g. start at 22:30 when the video gets into examples.
I'm surprised at this, do Go interfaces really introduce much overhead? Of course this depends on the level of performance you care about but surely, being a statically typed language, lots of the same optimisations are available.
and for the implementation of the compiler to remain simple
Basically, "interfaces" in Go and C++ actually refer to quite different language features. (Or at least, the author is using the term to describe quite different language features.)
C++ can optimize interface indirection away because it supports static polymorphism, which allows the compiler to generate specialized code for each concrete type used with a generic interface, eliminating the need for dynamic dispatch.
I believe it's a false dichotomy.
My thought is still that structural supersedes nominal.
A nominal interface is just another constraint added to the list of constraints of an underlying structural interface?
If you have compile-time only constants you can model nominals with structural,
type Square
static const IsSquare = true
var length = 10
You can kinda hack-in subtyping, type Shape
static const Shape = true
type Square
import static from Shape
static const IsSquare = true
var length = 10A consequence of this is that in Rust, which has a nominal system, you can implement two traits that contain a method with the same name and are required to disambiguate at the call site. In Go you can't do that, since the method is part of the concrete type.
The additional naming constraint added to a structural interface would form a sort of namespace for methods.
I think in the comments below that someone likens this to tags in C++.
For it to work, you need to add a namespace to all the colliding methods (a simple one would be a prefix like people do in C).
A nominal system is a more constrained structural system in some ways, but the opposite is true as well, so it's not as simple as 'nominal is subset of structural'.
A nominal type system still is superseded by a structural type system.
The difference is in how a type is defined. Or what kind of constraints are in entailment said otherwise.
An interface enforces constraints. The difference here is merely that the current implementations only have either one of these type of interfaces. So for the structural type system, all methods are in the global namespace, somehow.
That's all. Because our current languages are this way doesn't mean that the two concepts cannot be reconciliated or that one is just better than the other.
> That's all. Because our current languages are this way doesn't mean that the two concepts cannot be reconciliated or that one is just better than the other.
I don't think I agree with this though, I believe they're fundamentally different. The whole point of structural constraints is that they don't need the type to be aware of them. The point of nominal constraints though is that they require the type to explicitly acknowledge them.
In an ideal situation, everyone names and types things the same ('logical') way, so structural constraints 'just work'. A type implements has_organ, and an interface requires has_organ, and the type is automatically compatible with the interface.
A nominal system is the opposite though; the type explicitly understands what a specific interface implies and formally states it.
I just can't see how there's a subset-superset relationship, or how they can somehow be reconciled.
A nominal type system doesn't necessarily enforce semantics either.
It just enforces the location of a method definition.
Seen that way, because the relation is dual, one could indeed claim that a structural interface is a nominal interface where the name constraint is elided.
But just as in subtyping, one less constraint also means bigger set.
Of course if one were to decide that an object satisfying a nominal interface doesn't satisfy the structural interface obtained by ignoring the namespace, then I'd agree as well, these concepts would be disjoint.
I don't think they are though but I don't know of a language that ever mixed both either.
template<class T> concept serializable = requires { typename T::is_serializable_tag; };
void serialize(serializable auto x) {...};
struct NotSerializableClass { ... };
struct SerializableClass { using is_serializable_tag = void; ... };
serialize(NotSerializableClass{}); // error
serialize(SerializableClass{}); // all good
In C++, specializing a trait is also an option. So, while nominal and structural interfaces are not the same, sometimes the lines are blurred.The C++ 20 Standard Library provides numerous concepts which have a very different semantic requirement than the syntax they're checking. If you violate the syntactic requirement of course that'll earn you a compiler error, but if you violate the semantic requirements that's silently an ill-formed C++ program, it has no meaning whatsoever and might do absolutely anything if run.
If these were nominal, we could say, well, nobody should have deliberately implemented this inappropriate Concept, similar to an unsafe Rust trait, the act of implementation is a promise to others. But C++ Concepts aren't nominal and so there was no opportunity to do that and so in practice such deviations are likely very common despite the potentially drastic consequences.
[1] yes, concepts as an explicit language feature are new, but C++ has had de-facto concepts since Stepanov work on the original STL in the 90s.
But alas the problem here is IFNDR [Ill-formed No Diagnostic Required] so the compiler can't help you. All semantic constraints are your problem as the programmer, C++ decided that it's not the compiler's concern whether the program meets semantic constraints. Testing doesn't necessarily help at all, which is probably surprising.
Meta: I think the very first sentence is the victim of some drive-by editing, and needs one more pass. I'm not a native speaker, but I still suggest changing
In an earlier blog post, I showed on the Go programming language allow you to write generic functions once you have defined an interface.
Into perhaps
In an earlier blog post, I showed how the Go programming language allows you to write generic functions once you have defined an interface.
Considering the audience and author, I would seriously consider omitting the explanation of what Go is, but that's just polish. :)
> Of course, it also limits to the tools that I use to program: they cannot much about the type I am going to have in practice within the count function.
I think dropping the 'me' is often a feature of those whose native language is eastern European/Russian. The second (possibly) missing 'know' seems to support just editing mistakes. The structure of both makes me think of Portuguese for some reason!
To avoid going off topic on HN, something... something... ChatGPT
uint32_t next() { index++; return array[index - 1]; }
I do like uint32_t next() { return array[index++]; }
, shame it's kinda unintuitive.Essentially, in the past you would have your template code, say something dumb like:
template <typename T> T halve(T t) { return t / 2; }
and if you instantiated with the wrong type, say: halve("foo")
you get an error message pointing to the t/2 in the halve implementation. As your templates become less trivial, so do the error messages. So we could apply concepts: template <typename T> concept Halvable = requires (T t) { t / 2; };
template <Halvable T> T halve(T t) { return t / 2; }
Now our call to halve("foo") will complain that const char* is not Halvable. It will also produce an error similar to the original saying that we don't conform to Halvable because the `t / 2` expression fails. In this case it's not super valuable, but if you were instantiating a type or method that was more complicated instead of getting dozens or hundreds of errors in the instantiated body of the template, you just get an early error saying "you aren't conforming to X for these reasons:...".Unfortunately this does not stop you writing a template that depends on things that your concepts don't guarantee. For example, if we can halve something, we must be able to double it, right? (silly example to demonstrate the issue)
template <Halvable T> T double_it(T t) { return t * 2; }
Note, Halvable doesn't ensure that * is available, but this template is still "correct".Now we can do `double_it(1)` and that will work, but `double_it("foo")` will say the error is at the t * 2 in the body, when we probably want the error to actually at the point we try to call double_it.
Preventing this kind of error is non-trivial (possibly actually impossible?) given how concepts are defined. It's literally just a list of statements and expressions that need to be valid for a type. But going from a list of "these statements and expressions are valid" to "is this specific expression or statement valid in a template" is at best nontrivial. This is core limitation of the entire feature.
I think the core problem (don’t know if this is still the case, haven’t actually used concepts) is that templates raise errors at the point of expansion, rather than at the point of definition, because of how they are specified/implemented.
I think it’s possible that the compiler could do something smart by auto-creating a temp type that is the minimal possible implementation of the concept and attempting to compile the function. Any errors that result, should be flagged at the concept specified in the function.