← Code Compare

Bit Manipulation

Bit twiddling is the heart of systems programming: flags packed into a single word, hardware registers, bitsets. The task is identical in all seven languages - start from a value, set a bit with | (1 << n), clear it with & ~(1 << n), test it with (x >> n) & 1, then print the result in binary and hex. The integer lives in a register or on the stack, so this topic touches no heap - it's a pure look at each language's bitwise operators (&, |, ^, ~, <<, >>) and the helpers some of them add: C++'s std::bitset, Zig's @shlExact/std.math, Odin's bit_set, and Forth's hand-rolled masks.

Show: CC++HolyCZigHareOdinForth
C
#include <stdio.h>
#include <stdint.h>

/* Bit helpers as the canonical C macros. n is the bit index (0 = LSB). */
#define SET_BIT(x, n)   ((x) |  (1u << (n)))   /* OR  in a 1 */
#define CLEAR_BIT(x, n) ((x) & ~(1u << (n)))   /* AND in a 0 (mask is ...1101...) */
#define TEST_BIT(x, n)  (((x) >> (n)) & 1u)    /* shift the bit down, mask it off */

/* Print the low 8 bits of v as binary, MSB first. */
static void print_bin8(uint8_t v) {
    for (int i = 7; i >= 0; i--)
        putchar((v >> i) & 1u ? '1' : '0');
    putchar('\n');
}

int main(void) {
    uint8_t x = 0x0A;            /* 0000 1010 */

    x = SET_BIT(x, 0);          /* turn on bit 0 -> 0000 1011 */
    x = CLEAR_BIT(x, 3);        /* turn off bit 3 -> 0000 0011 */

    print_bin8(x);              /* 00000011 */
    printf("hex = 0x%02X\n", x); /* 0x03 */
    printf("bit 1 set? %d\n", TEST_BIT(x, 1)); /* 1 */
    return 0;
}

The three idioms are textbook C macros: set with | (1<<n), clear with & ~(1<<n), test with (x>>n)&1. x is a stack uint8_t and the masks are computed in registers, so bit work here costs zero allocations - there is no heap to malloc or free.

C++
#include <bitset>
#include <cstdint>
#include <iostream>

int main() {
    std::uint8_t x = 0x0A;        // 0000 1010

    // Raw operators, just like C: set, clear, test.
    x |=  (1u << 0);             // set   bit 0 -> 0000 1011
    x &= ~(1u << 3);             // clear bit 3 -> 0000 0011
    bool bit1 = (x >> 1) & 1u;   // test  bit 1

    // The STL helper: std::bitset wraps a fixed-width word with named ops.
    std::bitset<8> bits{x};      // value lives inline, no heap
    bits.set(2);                 // bits.set/reset/test/flip read clearly

    std::cout << "bits:    " << bits << '\n';        // 00000111
    std::cout << "hex:     0x" << std::hex << (int)x << '\n'; // 0x3
    std::cout << "bit 1?   " << bit1 << '\n';        // 1
    std::cout << "count:   " << bits.count() << '\n'; // popcount = 3
}

Modern C++ still uses the raw |/&/~/>> operators, but std::bitset<N> is the idiomatic helper: .set(), .reset(), .test(), .flip(), and .count() (popcount) read far clearer than masks. bitset<8> stores its bits inline (no heap, no new/delete), so there is nothing for RAII to clean up.

HolyC
// Top-level code runs in TempleOS -- no main() needed.
// HolyC's operators are C's: & | ^ ~ << >>.

U0 BitPrint(U8 v) {  // print low 8 bits, MSB first
  I64 i;
  for (i = 7; i >= 0; i--)
    Print("%d", (v >> i) & 1);
  Print("\n");
}

U8 x = 0x0A;          // 0000 1010

x |=  (1 << 0);       // set   bit 0 -> 0000 1011
x &= ~(1 << 3);       // clear bit 3 -> 0000 0011

BitPrint(x);          // 00000011
Print("hex = 0x%02X\n", x);              // 0x03
Print("bit 1 set? %d\n", (x >> 1) & 1);  // 1

HolyC inherits C's bitwise operators verbatim, so set / clear / test are the same |, & ~, and >>/& expressions. x is a plain U8 on the task's stack - the per-task heap is never touched, so there is no MAlloc/Free. Runs only on TempleOS.

Zig
const std = @import("std");

pub fn main() void {
    var x: u8 = 0x0A; // 0000 1010

    // Zig shifts require the shift AMOUNT to be a small unsigned type (u3 for u8),
    // which is checked at comptime -- shifting past the width is a bug, not UB.
    x |= @as(u8, 1) << 0; // set   bit 0 -> 0000 1011
    x &= ~(@as(u8, 1) << 3); // clear bit 3 -> 0000 0011
    const bit1: u1 = @truncate(x >> 1); // test  bit 1

    // std.math has typed helpers; @popCount is a builtin.
    std.debug.print("bin = {b:0>8}\n", .{x}); // 00000011
    std.debug.print("hex = 0x{X:0>2}\n", .{x}); // 0x03
    std.debug.print("bit 1? {d}\n", .{bit1}); // 1
    std.debug.print("popcount = {d}\n", .{@popCount(x)}); // 2
}

Zig makes bit widths explicit: a shift amount for a u8 must fit in a u3, and the literal 1 is widened with @as(u8, 1) so the mask is the right type - overflow is a comptime/safety error, never silent UB. @popCount is a builtin and {b}/{X} format binary/hex. x is a stack var, so no allocator is involved.

Hare
use fmt;

// Hare's bitwise operators: & | ^ ~ << >>.
export fn main() void = {
	let x: u8 = 0x0A; // 0000 1010

	x |= (1u8 << 0); // set   bit 0 -> 0000 1011
	x &= ~(1u8 << 3); // clear bit 3 -> 0000 0011
	const bit1: u8 = (x >> 1) & 1; // test  bit 1

	// Print the low 8 bits MSB-first.
	for (let i = 7i; i >= 0; i -= 1) {
		fmt::print((x >> (i: u8)) & 1)!;
	};
	fmt::println()!;
	fmt::printfln("hex = 0x{:x}", x)!; // 0x3
	fmt::printfln("bit 1? {}", bit1)!; // 1
};

Hare's bitwise operators mirror C (&, |, ^, ~, <<, >>), and untyped integer literals like 1u8 carry their type explicitly. The shift index is cast to u8 to match x. Everything is a stack let/const with no alloc/free; the trailing ! propagates any I/O error rather than ignoring it.

Odin
package main

import "core:fmt"

main :: proc() {
	x: u8 = 0x0A // 0000 1010

	// Raw operators: Odin uses ~ for XOR and &~ for AND-NOT (bit clear).
	x |= (1 << 0) // set   bit 0 -> 0000 1011
	x &~= (1 << 3) // clear bit 3 -> 0000 0011  (&~ is Odin's clear idiom)
	bit1 := (x >> 1) & 1 // test  bit 1

	// Odin's first-class helper: bit_set is a typed set of enum/range members,
	// stored as a single integer -- add/remove/test read like set operations.
	Flag :: enum {A, B, C, D}
	flags: bit_set[Flag]
	flags += {.A, .C} // set bits A and C
	flags -= {.A}     // clear bit A

	fmt.printf("bin = %08b\n", x)          // 00000011
	fmt.printf("hex = 0x%02X\n", x)        // 0x03
	fmt.println("bit 1?", bit1)            // 1
	fmt.println("flags:", flags, "C in?", .C in flags) // {C} true
}

Odin spells XOR as ~ and AND-NOT (clear) as &~, so x &~= mask is the idiomatic bit-clear. Its standout helper is bit_set[T] - a set of enum members packed into one integer, manipulated with += {…} / -= {…} and tested with in. x and flags are stack values, so the implicit context.allocator is untouched.

Forth
\ Forth has the bitwise primitives AND OR XOR INVERT LSHIFT RSHIFT.
\ (INVERT is bitwise NOT; there is no built-in ~, &~, etc.)
\ Build the three helpers as words; the value rides on the data stack.

: BIT     ( n -- mask )   1 SWAP LSHIFT ;        \ 1 shifted left n times
: SET     ( x n -- x' )   BIT OR ;               \ OR in the mask
: CLEAR   ( x n -- x' )   BIT INVERT AND ;        \ AND with ~mask
: TEST    ( x n -- f )    RSHIFT 1 AND ;          \ shift down, mask low bit

\ Print the low 8 bits of x, MSB first.
: .BIN8   ( x -- )
  8 0 DO                ( loop i = 0..7 )
    DUP 7 I - RSHIFT 1 AND .   \ print bit (7-i)
  LOOP DROP CR ;

10                      \ x = 0x0A = 0000 1010 on the stack
0 SET                   \ set   bit 0 -> 0000 1011
3 CLEAR                 \ clear bit 3 -> 0000 0011
DUP .BIN8               \ 0 0 0 0 0 0 1 1
DUP 1 TEST .            \ test bit 1 -> 1
HEX . DECIMAL CR        \ print value in hex -> 3

Forth gives you the raw primitives AND OR XOR INVERT LSHIFT RSHIFT and nothing else, so set/clear/test are defined as words that operate on the value already on the data stack - INVERT AND is the clear idiom (no &~). HEX/DECIMAL switch the numeric base for . rather than formatting a string. No variables, no heap: every intermediate lives on the stack, so nothing is allocated or freed.