Carbon Copy No.12: Generics Part IV Specialization

22 views
Skip to first unread message

Daniel 'Wolff' Dobson

unread,
Apr 30, 2026, 7:12:09 PMApr 30
to anno...@carbon-lang.dev

Carbon Copy, April 2026

Here is the new Carbon Copy, your periodic update on the Carbon language!

Carbon Copy is designed for those people who want a high-level view of what's happening on the project. If you'd like to subscribe, you can join anno...@carbon-lang.dev. Carbon Copy should arrive roughly (sometimes extremely roughly) every other month.

Toolchain update

In our last update, we showed interop, where Carbon called C++ seamlessly. Let's now take a look at C++ calling Carbon. (As we're all Carbon developers here, we call this "reverse interop".) 


Here's our Carbon code for a vending machine base class:

class Snack {

  var price: i64;

};


base class VendingMachine {

  // A factory function to initialize the Carbon base class

  fn Create(snack_price: i64) -> VendingMachine {

    return {.snack_price = snack_price};

  }


  // Returns a Snack

  fn DispenseItem[self: Self]() -> Snack {

    // ... internal logic to retrieve a snack ...

    return {.price = self.snack_price};

  }


  var snack_price: i64;

}


In C++, we can extend that base class and call a method that returns a pointer:


// C++ class extends the Carbon base class

class SnackMachine : public Carbon::VendingMachine {

 public:

  SnackMachine() : Carbon::VendingMachine(

      Carbon::VendingMachine::Create(5)) {}


  Carbon::Snack Vend() {

    // Call a method inherited from the Carbon base class.

    return DispenseItem();

  }

};


int main() {

  SnackMachine machine;

  Carbon::Snack my_snack = machine.Vend();

  std::cout << "Price = " << my_snack.price;

  return 0;

}


See it in Compiler Explorer!

Spotlight: Specialization

In this spotlight, we're going to take a look at generics again. (Will we run out of generics topics in Carbon? It is not this day.)


C++ provides mechanisms for specialization in templates. In this example of C++, we're going to create a template to handle crossing various kinds of legendary animals. Our first is the jackalope:


// THIS IS C++

#include <iostream>


struct Jackrabbit {};

struct Antelope {};


// Generic fallback for crossing any two animals

template <typename One, typename Two>

void Cross(One m, Two f) {

  std::cout << "a generic legendary animal\n";

}


// "Partial specialization" via overloading: Any animal + Antelope

template <typename One>

void Cross(One m, Antelope f) {

  std::cout << "something-antelope\n";

}


// Fully-specialized for Jackrabbit and Antelope

void Cross(Jackrabbit m, Antelope f) {

  std::cout << "jackalope\n";

}


// THIS WAS STILL C++


C++ forbids the partial specialization of function templates, so we must use an overload. (Compiler explorer version.)

Carbon: One way, and it's consistent

Switching over to Carbon, the approach fundamentally changes. In Carbon, you cannot directly specialize types or functions. Instead, Carbon relies on interfaces as its only static open extension mechanism. Specialization is done by delegating to interfaces and defining parameterized impl declarations that apply to various types.


To replicate the same logic in Carbon, and more, we start by defining an interface for the crossing operation.

import Cpp library "<iostream>";

private alias std = Cpp.std;


interface CrossableWith(U:! type) {

  fn Cross[self: Self](other: U);

}


fn Cross [U:! type, T:! CrossableWith(U)] (t:T, u:U) {

  t.Cross(u);

}


class Jackrabbit {}

class Antelope {}


// A blanket impl that applies to any two types

// anything->anything (makes a "legendary animal")

impl forall [T:! type, U:! type] T as CrossableWith(U) {

  fn Cross[unused self: T](unused other: U) {

    std.cout << "legendary animal\n";

  }

}


Our generic looks about the same as C++, although we introduce a new keyword, forall.


The forall keyword is used to declare a parameterized impl (also known as a generic impl declaration). It introduces a list of compile-time generic parameters in square brackets immediately following the impl keyword, formatted as:


impl forall [ *generic parameters* ] *type* as *constraint*


Now that we've made a generic legendary animal cross, let's start specializing around jackrabbits.


// #1 jackrabbit->anything (get a jacka-something)

impl forall [U:! type] Jackrabbit as CrossableWith(U) {

  fn Cross[unused self: Jackrabbit](unused other: U) {

    std.cout << "jacka-something\n";

  }

}


The next two impls for antelope/antelope and jackrabbit/antelope are fully-specified, so they are final.  We could write the final keyword, but by fully-defining all the types (or having no parameters at all), they are effectively final, and function as final.


// A fully-specialized impl for antelope->antelope

impl Antelope as CrossableWith(Antelope) {

  fn Cross[unused self: Antelope](unused other: Antelope) {

    std.cout << "another antelope";

  }

}


// A fully-specialized jackrabbit->antelope

impl Jackrabbit as CrossableWith(Antelope) {

  fn Cross[unused self: Jackrabbit](unused other: Antelope) {

    std.cout << "jackalope";

  }

}


A dilemma: Which came first, the jack or the lope?

We have a problem here, one familiar to template writers worldwide. Were I to cross a jackrabbit and an antelope (or rather a Jackrabbit and an Antelope), what if more than one impl matches?


Carbon uses a strict, step-by-step selection algorithm when multiple impl declarations match a type and interface. The process of determining which specialization (or impl) to use is designed to guarantee implementation coherence, meaning the compiler must predictably choose the same implementation for a given query, regardless of which libraries are imported.


When crossing ancestors in our example, we have 

  • a generic fallback

  • a non-specific Jackrabbit fallback, and

  • fully-specialized antelope and jackalope crosses.


When figuring out which specialization to use, there are three rules. Let's take a look at each one.

Rule 1: final rule

The first rule here is the final rule, not the last rule, if you follow my drift.


  • Rule 1, the final rule: Find a final implementation (or, as above, effectively final). 


fn Run1() {

  let one: Jackrabbit = {};

  let two: Antelope = {};


  // Output: "jackalope"

  Cross(one, two);

}


In this case, the final rule does the work here. It chooses impl Jackalope as CrossableWith(Antelope).

Rule 2: Overlapping rule

Consider this case where we’re trying to create a skvader, the legendary Swedish hare-grouse:


// Same interface as above


// More animals to cross

class Hare {};

class Grouse {};


impl forall [U:! type] Hare as CrossableWith(U) {

  fn Cross[unused self: Hare](unused other: U) {

    std.cout << "hare-something\n";

  }

}


impl forall [T:! type] T as CrossableWith(Grouse) {

  fn Cross[unused self: T](unused other: Grouse) {

    std.cout << "something-grouse\n";

  }

}


fn Run2() {

  let one: Hare = {};

  let two: Grouse = {};


  // Outputs "hare-something"

  Cross(one, two);

}


In this case, with two non-final methods, the Overlap rule applies.


  • Rule 2: Overlap rule: If no final impls match, Carbon evaluates all matching non-final impls based on their "type structure", called the "overlap rule". 

    • To determine the type structure, the compiler replaces all generic parameters in the impl with question marks (?). 

    • Carbon then compares the type structures by reading them from left to right to find the first difference.

    • The impl that has a concrete type (a non-?) at that first difference is considered the most specific and wins.


For our example above, the (Hare ?) example would be chosen as compared to (?, Grouse), since (Hare ?) has the only non-? in the arguments going left-to-right. (Carbon explorer example.)

Rule 3: Prioritization rule

Sometimes, multiple impl declarations will have the exact same type structure (that is, they overlap without one being strictly more specific). If this happens, Carbon requires that all of those impl declarations be grouped together in the same match_first block (also called a prioritization block).


interface Horsey {}

interface Swimmy {}


// Just a regular Hippo, like you'd find in the Nile

class Hippo {}


// A Hippocampus is a legendary swimming horse hybrid (and, separately, a

// part of the brain)

class Hippocampus {}

impl Hippocampus as Horsey {}

impl Hippocampus as Swimmy {}


// Both of the following impls have the exact same type structure: 

//    `? as CrossableWith(Hippo)`.

// Because a Hippo satisfies both constraints, the compiler needs 

// an explicit priority ranking.

match_first {

  // Priority 1: Horsey animals crossed with a Hippo

  impl forall [T:! Horsey] T as CrossableWith(Hippo) {

    fn Cross[self: T](other: Hippo) {

      std.cout << "Horsey-Hippo cross\n";

    }

  }


  // Priority 2: Swimmy animals crossed with a Hippo

  impl forall [T:! Swimmy] T as CrossableWith(Hippo) {

    fn Cross[self: T](other: Hippo) {

      std.cout <<"Swimmy-Hippo cross\n";

    }

  }

}


fn Run3() {

  let one: Hippocampus = {};

  let two: Hippo = {};


  // Outputs "Horsey-Hippo cross"

  Cross(one, two);

}


  • The Prioritization rule: When resolving the match, Carbon looks at this block and selects the first matching impl in the exact order they are listed. (Note: As of today, this is not implemented in the toolchain.)


You can see here we've prioritized Horsey vs Swimmy to match.

Final Thoughts

Specialization in Carbon only applies to templates, and when it does, there are three simple rules to remember for managing specialization ordering:


  • The final rule

  • The Overlap rule

  • The Prioritization rule


And that's it!

Recent proposals and issues

If you want to keep up on Carbon’s proposals and issues, follow "Last Week In Carbon". Check out the RSS feed!

Recently accepted proposals including:



Recently closed leads issues including:


Wrap-up

Don't forget to subscribe! You can join anno...@carbon-lang.dev. If you have comments or would like to contribute to future editions of Carbon Copy, please reach out. And, always, join us any way you can!


Next year in Andalusia,
Wolff, Josh, and the Carbon team

Reply all
Reply to author
Forward
0 new messages