Carbon Copy No.11: Generics Part III Parameterized Types

12 views
Skip to first unread message

Daniel 'Wolff' Dobson

unread,
Mar 5, 2026, 2:49:59 PMMar 5
to anno...@carbon-lang.dev

Carbon Copy, March 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

We've shown in earlier toolchain updates that you can import C++ easily. Today, we'll look at using the C++ standard library. 


import Cpp library "<map>";

import Cpp library "<string_view>";

import Cpp library "<vector>";

private alias std = Cpp.std;


fn LookUp(s: str, n: u64) -> i32 {

  var a: std.map(std.string_view, std.vector(i32)) = (

    ("hello", (1, 2, 3)),

    ("world", (4, 5))

  );

  return a[s][n];

}


Carbon uses Clang to give complex initializers the same semantics as in C++, including overload resolution, default arguments, constructor templates, and conversion functions. Existing complex C++ APIs have delicate and nuanced overload sets, so it's important to model initialization and overload resolution the same way as C++ in order to avoid surprising behavior.


Another example:


import Core library "range";

import Cpp library "<vector>";

import Cpp library "<iostream>";


private alias std = Cpp.std;


fn PrimesLessThan(n: i32) -> std.vector(i32) {

  returned var primes: std.vector(i32) = ();

  var composite: std.vector(i8) =

    std.vector(i8).vector((n + 1) as u64, 0);

  for (i: i32 in Core.InclusiveRange(2, n)) {

    if (composite[i as u64] == 1) {

      continue;

    }

    primes.push_back(i);

    for (j: i32 in Core.InclusiveRange(2, n / i)) {

      composite[(i * j) as u64] = 1;

    }

  }

  return var;

}


fn Run() {

  var primes: std.vector(i32) = PrimesLessThan(100);

  for (i: i32 in Core.Range(primes.size() as i32)) {

    (std.cout << primes[i as u64]) << "\n";

  }

}


Here we are calling methods on std::vector such as push_back. These methods are selected by C++ overload resolution, and instantiated from templates. We're also showing that you can do overloaded array access, and use streaming operators on cout


There are extra casts because, unlike C++, Carbon doesn't allow us to discard the i32 sign bit, even though we use C++ rules to select the overload. Without the conversion, we'd select a C++ operator[](size_t) overload, and then reject the attempt to call it because there's no implicit conversion from i32 to u64 in Carbon.

Spotlight: Generics Part III

Let's say we were building a smart lamp that locally keeps track of how many elk are attending a cocktail party so it knows when it can turn on and off the lights. Assuming it's a narrow party room so guests must leave in the opposite order they arrived, we could use a stack.


In Carbon, you can use C++ functionality like the standard template library.


import Cpp library "<stack>";

import Core library "io";


fn Run() {

   // Let's assume our elk are identified by number

   var s: Cpp.std.stack(i32);


   // Elk #5 arrives

   s.push(5);

   // Who was the last to arrive?

   Core.Print(s.top());

   // Elk #5 leaves

   s.pop();

}

Example: https://godbolt.org/z/ev3cP75G1


Let's replicate the stack functionality in Carbon with the same flexibility between types, which is to say you could make a Stack of nearly anything.

An elk party isn't an elk party until everyone gets there

As we've discussed before, Carbon has two generics systems, checked generics and template generics (familiar from C++). In this case, we'll again use checked generics, as the intention is that checked generics are favored for API definitions.


Let's take a look at some code. (There's a running example on Compiler Explorer for reference.)


class Stack(T:! Core.Copy, N:! i32) {

  // Some forward declarations

  fn Clear[ref self: Self]();

  fn Empty[self: Self]() -> bool;

  fn Push[ref self: Self](x: T) -> bool;

  fn Pop[ref self: Self]() -> T;


  private var size: i32;

  private var contents: array(T, N);


  // ... continued below!

}


:! syntax indicates that T and N are compile-time bindings. 


We're adding a requirement that any T must support the Core.Copy interface. This means just any random type won't work, but most built-in types do, like i32 and char. More on how Core.Copy is used below.


The value of T is a type representing what kind of elements can be stored in the stack, and N is a compile-time parameter representing the maximum capacity of the stack.


With checked generics, the definition of Stack is fully type-checked by the compiler at the time it is defined, no matter what types of T and N you use.

Sidebar: What is Core?

“Core” is the Carbon standard library. Part of it, known as "the prelude", is implicitly imported into every Carbon source file. This means fundamental types and interfaces are immediately ready to use, and this includes Core.Copy. Other parts need to be explicitly imported using the Core package name; in these examples, we need to import "io" from Core to get Core.Print

Making a stack

Let's take a look at some implementations, written inside the class definition above.


class Stack(T:! Core.Copy, N:! i32) {

  // ... continued from above.


  fn Clear[ref self: Self]() {

    self.size = 0;

  }


  fn Empty[self: Self]() -> bool {

    return self.size == 0;

  }


  // ... continued below!

}



This code is pretty straightforward. You may notice a (relatively) recent design change here: We now support reference parameters, which are marked with the ref keyword, and we use that (instead of the obsolete addr) to make self mutable . Among other things, this means members are consistently accessed using "self." (and never "self->").


Let's add some critical functions that use T:

class Stack(T:! Core.Copy, N:! i32) {

  // ... continued from above.


  fn Push[ref self: Self](x: T) -> bool {

    if (self.size < N) {

      self.contents[self.size] = x;

      ++self.size;

      return true;

    }

    return false;

  }

// End of class definition

}


// Defining this method outside the class definition

// To indicate the type we use exactly the tokens that follow 

// `class` above.

fn Stack(T:! Core.Copy, N:! i32).Pop[ref self: Self]() -> T {

  if (self.size > 0) {

    --self.size;

  }

  return self.contents[self.size];

}


Push operates on our compile-time type T and does some basic stack stuff with it.


The lines self.contents[self.size] = x and return self.contents[self.size] are why T needs to support Core.Copy. We're making a copy of x when storing or returning it. (You could make an array of pointers, but then you'd copy the pointer.) 


Let's use our stack.


fn Run() {

  var s: Stack(i32, 4);

  s.Clear();

  // Two elks come to visit

  s.Push(1);

  s.Push(2);


  while (not s.Empty()) {

    Core.Print(s.Pop());

  }

  // Program output:

  // 2

  // 1

}


Parameterized types allow you to avoid writing near-duplicate definitions for different types. We'll upgrade our party-tracker to track drinks, too, by making T a struct type (see Carbon Copy #5 for more on those).


fn Run() {

  var s: Stack({.elk: i32, .drink: char}, 4);

  s.Clear();


  // An elk orders coffee

  s.Push({.elk = 5, .drink = 'c'});


  // This elk has a martini, shaken, not stirred

  s.Push({.elk = 0x007, .drink = 'm'});


  while (not s.Empty()) {

    let bob: {.elk: i32, .drink: char}  = s.Pop();

    Core.PrintChar(bob.drink);

    Core.PrintChar('\n');

  }

  // Program output:

  // m

  // c

}

Final thoughts

You can see, then, that parameterized types can be quite useful. Container classes are just the start.


The above example of a stack of structs doesn't quite work yet, as struct types don't implement Core.Copy yet. (You can follow along on the issue.) You could replace the struct with a containing class called something like DrinkingElk, and then that class would have to implement Copy

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!


Carbon has the need for speed,
Wolff, Josh, Richard, and the Carbon team


Reply all
Reply to author
Forward
0 new messages