hyperoperations in c++

abuse of syntax has never been so fun
dec 7, '25

nobody:
me: what if c++ had python's exponentiation operator?
nobody: …
me: oh, wait, what if i did the entire hyperoperator sequence!?

dear reader, i did.

int main() {
  integer n = 3; // or whatever
  cout
    << "  2·3 = " << setw(5) << 2 * n << '\n'
    << "   2³ = " << setw(5) << 2 ** n << '\n'
    << " 2↑↑3 = " << setw(5) << 2 *** n << '\n'
    << "2↑↑↑3 = " << setw(5) << 2 **** n << '\n'
    ;
}
g++ -Wall -Wpedantic -o hyperop hyperop.c++ 2>&1 &&
    ./hyperop
  2·3 =     6
   2³ =     8
 2↑↑3 =    16
2↑↑↑3 = 65536

why i did this

because it's a horrible idea. i'm not the only one with horrible ideas: check out https://github.com/klmr/named-operator.

as a recovering c++ weenie, i think that the ability to change the meaning of the goddamned language is somewhat horrifying, because even simple statements like a + b can devolve into hundreds of lines of template errors — and then, when fixed, wind up mutating global state and making you sad anyway when you have a crash in prod.

of course, as a recovering c++ weenie, i know exactly how to make these horrible things happen and have a deep, pent-up rage/delight in seeing things crash, burn, then keep flying, so i needed an outlet.

what are the hyperoperations?

the hyperoperator sequence is what you get if exponentiation doesn't satisfy you. their intuitive derivation is "multiplication is (for some value of is) just repeated addition, exponentiation is (again, for some value of is) , so why not define another operator to be repeated exponentiation?" this next operator is knows as tetration, and is the first in the sequence of hyperoperations.

for your reference (and mine, while i'm writing this), the hyperoperations may be defined either by the fully recursive formula god, i love org mode. all the writing, code, math, and output from this shitpost lives in exactly one file, and i can inline-edit, format, and run the code blocks, all from emacs. truly a dream come true. if you're curious about this and maybe want to fuck up your life too, go check it out.

\begin{equation} \label{orge731696} H_n (a, b) = a[n]b = \begin{cases} b + 1 & \text{if } n = 0 \\ a & \text{if } n = 1 \text{ and } b = 0 \\ 0 & \text{if } n = 2 \text{ and } b = 0 \\ 1 & \text{if } n \geq 3 \text{ and } b = 0 \\ H_{n - 1}(a, H_n(a, b - 1)) & \text{otherwise} \\ \end{cases} \end{equation}

or by the more readable, less formal set of expressions

\begin{align} a[1]b & = a + b \\ a[n]b & = a[n - 1](a[n - 1](\dots(a[n - 1]a)\dots)), \\ \end{align}

the latter of which is a pretty decent candidate for direct translation into code. both because it's easier to read and implement, and because the theoretically-lovely recursive formula given in \eqref{orge731696} is horribly inefficient if you implement it directly. we do actually want to be able to run this program, you know.

defining the sequence

we'll start with some pretty standard c++: a templatized function the astute reader will note that this didn't have to be a template. we could have written a simple recursive function call. but what are we, C programmers? this is Real Software, written by Segmentation fault (core dumped) and some specializations. nothing too crazy. i'll even use int as our numeric type!

 1template <int N>
 2int hyperop(int a, int b) {
 3  int acc = a;
 4  for (int i = 0; i < b - 1; ++i)
 5    acc = hyperop<N - 1>(a, acc);
 6  return acc;
 7}
 8
 9template<>
10int hyperop<0>(int, int b) {
11  return b + 1;
12}
13
14template<>
15int hyperop<1>(int a, int b) {
16  return a + b;
17}
18
19// for efficiency; we're all about zero-cost abstractions here
20template<>
21int hyperop<2>(int a, int b) {
22  return a * b;
23}

testing this, we see that it gives the results you'd expect from the intuitive definition of the hyperoperations:

int main() {
  cout
    << "2[0]3 = " << setw(5) << hyperop<0>(2, 3) << '\n'
    << "2[1]3 = " << setw(5) << hyperop<1>(2, 3) << '\n'
    << "2[2]3 = " << setw(5) << hyperop<2>(2, 3) << '\n'
    << "2[3]3 = " << setw(5) << hyperop<3>(2, 3) << '\n'
    << "2[4]3 = " << setw(5) << hyperop<4>(2, 3) << '\n'
    << "2[5]3 = " << setw(5) << hyperop<5>(2, 3) << '\n'
    << "2[6]3 = " << setw(5) << hyperop<6>(2, 3) << '\n'
    ;
}
g++ -o hyperop-easy hyperop-easy.c++ &&
    ./hyperop-easy
2[0]3 =     4
2[1]3 =     5
2[2]3 =     6
2[3]3 =     8
2[4]3 =    16
2[5]3 = 65536
2[6]3 =     2

oops. well, we just won't use that last one. no, i will NOT use bignums, you can't make me! (it overflows long, too.)

making it pretty

of course we're not done. who told you we were done!?

the real beauty of c++ is making the hard easy, the easy hard, and both cause unexpected template substitution errors. we want some convenient syntax for this, and anyone who says we're abusing how operators work be damned! the stl gets to cause unexpected behavior thanks to operator precedence not changing in an overload, so why shouldn't we?

since python uses ** as an exponentiation operator, we should use that as our syntax for exponentiation, too, just a little extended. c++, as you may know, does not have a ** operator, but it does have a unary * operator — pointer dereferencing — that's conveniently overloadable! so we'll just use that.

but, like, how? and how are we going to manage to chain them and get the right operator?

we'll start with some intermediate types, since you can't overload built-in operators and some operator overloads have to be members (they think they're sooooo darned special). so we'll pull a trick from java sorry, bjarne (pp. 22–23). and define our own numeric class for the right-hand side. we're also going to need some way to count how many asterisks we've typed — and we want to do that statically — which i've decided to do via an intermediate type that should disappear at runtime. so:

24// forward declaration because c++ is actually not very smart
25template <typename last>
26struct next_op;
27
28struct integer {
29  constexpr const static int N = 2; // "we will discuss this later"
30  int i;
31
32  // copy-constructible from int
33  integer(int i) : i(i) {}
34
35  // implicitly convertable to int, if we need to
36  operator int() const { return i; }
37
38  // will be defined out-of-line once we know more about next_op
39  next_op<integer> operator*();
40};
41
42// nice convenient literal syntax, totally not a confusing suffix
43integer operator""_i(unsigned long long i) { return integer(i); }

for the purposes of this shitpost, i have refrained from testing some of the more exotic operations you can perform on integers, such as addition. the conversion operator operator int should take care of all that for us.

anyway, that's integer defined. now, for next_op:

44template <typename last>
45struct next_op {
46  constexpr const static int N = last::N + 1;
47  const last& l;
48
49  // can create the next operator from the current one
50  next_op(const last& l) : l(l) {}
51
52  // can perform that construction by iterative dereferencing
53  next_op<next_op<last>> operator*() { return next_op<next_op<last>>(*this); }
54
55  // implicitly converts to... an integer? sure, why not?
56  operator int() const { return l; }
57};
58
59// finally define construction of next_op<integer>
60next_op<integer> integer::operator*() { return next_op<integer>(*this); }

the objective here being that we can "dereference" integer some number of times, then perform a final "multiplication" to actually apply the hyperoperator. speaking of:

61template<typename last>
62int operator*(int a, next_op<last> b) {
63  return hyperop<decltype(b)::N>(a, b);
64}

that should do it. i refer you back to the the beginning of this post.