Can Zig, Go, or Rust compete with C++ for Competitive Programming?
Published: 05-22-25
Competitive Programming
I’m not the best Competitive Programmer, in fact I think it would be an overstatement to call me a Competitive Programmer. I have probably participated in one competitive programming contest, but I have a deep admiration for people that perfect its craft. However, I do prefer practicing algorithms and interview problems using competitive programming because you need to handle real input. I love advent of code for the same reason, it feels like a more realistic exercise when you have to handle taking in and reporting data. This is nothing against leetcode, it is a great resource, especially for interview prep but feels like you are working in more of a vacuum.
As a non competitive, competitive programming enthusiast I have practiced problems with multiple languages, even languages that are somewhat ill suited for this task. If I had to do a real competition I would use C++, it combines a full standard library with performance and has been the most used competitive programming language forever.
In this post I am just exploring several languages and how to set them up for competitive programming, mainly handling input.
- Rust
- Go
- Zig
handling input with C++
- when completing a problem it is common practice to take the input into your program via a text file.
- this comes with the issue of not wanting to write two solutions, one for stdin and one for reading a file
- really simple and somewhat standardized as C++ is the defacto standard for competitive programming
- no thinking out of the box required
- online judges will pass flags disabling the
freeopen
calls
// use local files unless compiled with ONLINE_JUDGE flag
#ifndef ONLINE_JUDGE
freopen("input.txt", "r", stdin); // input
freopen("output.txt", "w", stdout); // output
#endif
int i;
// then we can get input from stdin or 'input.txt'
// with
std::cin >> i;
//or
scanf("%d", &i);
Getting input with Rust
- Rust is the new kid on the block
- it’s very loved (maybe too much)
- does everything need to be rewritten???
- great build system & package manager
- type safety and some memory safety guarantees
- also complicated but it seems like every added complexity has a purpose
- it’s very loved (maybe too much)
- types have to align and you have to defeat the borrow checker
- might seem frustrating at first but when it clicks it is elegant
- can’t use simple macros to switch between input types (file and stdin)
- manually typing input is painful and borderline impossible for some problems
- but if you set file input you will need to change your code to submit to an online judge
- output
- much less complicated just use
println!
- not the best for performance (or so i’ve heard)
- but the goal here is input and I think
println!
is fine
- but the goal here is input and I think
- much less complicated just use
- what I love about this Rust implementation is how the types are used
- it almost feels like magic being able to use
collect
with polymorphic types to make the function determine what type to parse from the input. - it almost feels like automated tetris where rust finds the type that fits.
- it almost feels like magic being able to use
handling file input
- example from Advent of Code
- leveraging the
include_str!
macro - embedds the file in the binary (good for AOC not for competitive programming)
- leveraging the
let input: Vec<Vec<Vec<Vec<&str>>>> = include_str!("../input.txt")
.lines()
.map(|l| {
let mut x: Vec<&str> = l.split(": ").collect();
x.reverse();
x.pop();
x.reverse();
x[0]
})
.map(|l| {
let x: Vec<&str> = l.split("; ").collect();
let y: Vec<Vec<&str>> = x
.into_iter()
.map(|g| g.split(", ").collect()).collect();
let z: Vec<Vec<Vec<&str>>> = y.into_iter()
.map(|g| g
.into_iter()
.map(|c| c.split(" ").collect()).collect()).collect();
z
})
.collect();
handling standard input
fn main() {
let mut buffer = String::new(); // empty String
let stdin = std::io::stdin(); // standard input
// read a line from stdin and assign to buffer
stdin.read_line(&mut buffer).unwrap();
println!("{}", buffer);
}
great starter template
se std::io::stdin;
fn take_int() -> usize { // get int from stdin
let mut input = String::new();
stdin().read_line(&mut input).unwrap();
return input.trim().parse().unwrap()
}
fn take_vector() -> Vec<usize> { // get a vector of usize
let mut input = String::new();
stdin().read_line(&mut input).unwrap();
let arr: Vec<usize> = input.trim().split_whitespace().map(|x| x.parse().unwrap()).collect();
return arr;
}
fn take_string() -> Vec<char> { // get a string as a vector of char
let mut input = String::new();
stdin().read_line(&mut input).unwrap();
let vec:Vec<char> = input.trim().chars().collect();
return vec;
}
fn to_string(vec:Vec<char>) -> String{ // convert vec of char to String
return vec.iter().collect::<String>();
}
making it a module
- wrap in a module, and make functions public
- easier importing
- no unused errors
mod rcp {
use std::io::stdin;
pub fn take_int() -> usize
//...
}
pub fn main() {
let i = rcp::take_int();
}
making it type polymorphic
- the Rust type system lets you tell the function what type to return
- achieved with type polymorphism, restricted with Traits
mod rcp {
use std::io::stdin;
pub fn read<T: std::str::FromStr>() -> T {
let mut line = String::new();
stdin().read_line(&mut line).expect("read_line failed");
line.trim().parse().ok().expect("failed to parse")
}
}
pub fn main() {
let message: String = read();
let float: f32 = read();
println!("{} {}", message, float);
}
adding file or standard input
- add a function to init a
reader
- corresponds to either a file or stdin
mod rcp {
use std::{
fs::File,
io::{self, BufRead, BufReader},
};
// returns a Boxed type that implements BufRead
pub fn init_reader() -> Box<dyn BufRead> {
if std::env::args().any(|arg| arg == "--file") { // `cargo run -- --file`
Box::new(BufReader::new(
File::open("input.txt").expect("failed to open 'input.txt'"),
))
} else {
Box::new(BufReader::new(io::stdin()))
}
}
...
}
final template
- putting it all together
- only downside is we need to pass the reader around
mod rcp {
use std::{
fs::File,
io::{self, BufRead, BufReader},
};
pub fn read<T: std::str::FromStr>(reader: &mut impl BufRead) -> T {
let mut line = String::new();
reader.read_line(&mut line).expect("read_line failed");
line.trim().parse().ok().expect("failed to parse")
}
pub fn read_vec<T: std::str::FromStr>(reader: &mut impl BufRead) -> Vec<T> {
let mut line = String::new();
reader.read_line(&mut line).expect("read_line failed");
line.split_whitespace()
.map(|s| s.parse().ok().expect("failed to parse"))
.collect()
}
// throw away empty lines
pub fn empty_line(reader: &mut impl BufRead) {
let mut dummy = String::new();
reader.read_line(&mut dummy).ok();
}
pub fn init_reader() -> Box<dyn BufRead> {
if std::env::args().any(|arg| arg == "--file") {
Box::new(BufReader::new(
File::open("input.txt").expect("failed to open 'input.txt'"),
))
} else {
Box::new(BufReader::new(io::stdin()))
}
}
}
using the template
pub fn main() {
let mut reader = rcp::init_reader();
let num: usize = rcp::read(&mut reader);
let word: String = rcp::read(&mut reader);
let vec: Vec<String> = rcp::read_vec(&mut reader);
println!("input -> {}, {}, {:?}", num, word, vec);
}
Getting input with Go
Go is a great language. Despite the hate it seems to receive from the PL community I think it’s great. It was designed for development, you have an idea and you don’t want to get caught up in the language you’re using. It’s not the best for research but for software engineering it is great.
It’s simplicity can be seen in the simplicity of using it to parse input from Stdin or from a file without changing your code.
How to set Stdin
- one of the nice things about Go is its standard library
- we can import
os
and tell Go what should be used as Stdin - we just need to “pipe” this text into Stdin
- wrapping this in a function allows us to use the same code for solving and submitting a problem, only having to comment out one line
- once our file is used for Stdin we can use the
fmt
functionScanf
as usual!
import (
"fmt"
"log"
"os"
)
func setStdin(file string) {
// text []byte -> compatible with os.Stdin
text, err := os.ReadFile(file)
if err != nil {
log.Fatal(err)
}
// reader and writer
r, w, err := os.Pipe()
if err != nil {
log.Fatal(err)
}
// set os.Stdin to our reader
// this will "read" what is written from our input file
os.Stdin = r
// ignore number of bytes written and write file data to writer
// this text will then be able to be read from our "reader" r
_, err = w.Write(text)
if err != nil {
log.Fatal(err)
}
// close writer
w.Close()
}
func main() {
setStdin("input.txt")
// we can now use `fmt.Scanf` as we would if we were reading from standard input
...
}
Getting input with Zig
Zig was the hardest language to get working. I really enjoy the language and appreciate its goals for low level programming. However, I don’t think I’ll ever write much Zig as most of my work and research falls outside of its intended use cases. I am not a low level programmer but if I become one Zig would be one of my tools for sure.
Trying to get input from a file without changing the code that reads from input with Zig’s standard library was too difficult.
So I leveraged one of Zig’s greatest strengths, it’s C FFI. C has scanf
and printf
, so I wrapped them with Zig functions to make their use more seamless.
importing C libraries with Zig
Importing a C library is fairly simple with @cImport
. However, when using C functions that return any value (scanf returns an int) that value has to be explicitly ignored.
But the _ = scanf(...)
is pretty cumbersome so I opted to wrap these functions.
const cStdio = @cImport({
@cInclude("stdio.h");
});
const cString = @cImport({
@cInclude("string.h");
});
String representations in Zig
Strings in Zig can be a little weird, mainly because they differ from C strings (arrays of chars), and might need to be massaged slightly to be used in imported C functions. In order to pass strings from Zig to C we use a special type…
[*:0]const u8
- null-terminated C string
Wrapping C functions with Zig
One of Zig’s most attractive features is the Union (or Sum) type. This let’s us write functions that can throw errors, denoted with !
.
To encapsulate possible errors from the scanf
and printf
functions I created the CStudio
error type
const CStdioError = error{
printfError,
scanfError,
fgetsError,
freopenError,
};
It is now easy to wrap a function and show that it can fail. For example, this is how scanf
is wrapped.
anytype
- accept any value(s), variadic-style
@call(.auto, ...)
- call a function with automatic calling convention
.{a} ++ b
- build a new tuple starting with b, then appending b
// `scanf` wrapper
fn c_scanf(restrict: [*:0]const u8, args: anytype) !void {
if (@call(.auto, cStdio.scanf, .{restrict} ++ args) < 1)
return CStdioError.scanfError;
}
Using the wrapped function feels almost native and allows for the competitive programming flow many are used to.
var i: i32 = undefined;
var s: [30]u8 = undefined;
try c_scanf("%d", .{&i});
// in C `s` decays into a pointer, not in zig!
try c_scanf("%s", .{&s});
Setting standard input to a file
This was “more of the same” in that I opted to wrap C functions for the task. However, due to a known number of function arguments it is much more simple.
// set `stdin` and `stdout` to files
// must take in `[*:0]const u8` to be compatible with C
fn fileIO(input: [*:0]const u8, output: [*:0]const u8) !void {
_ = cStdio.freopen(input, "r", cStdio.stdin()) orelse
return CStdioError.freopenError;
_ = cStdio.freopen(output, "w", cStdio.stdout()) orelse
return CStdioError.freopenError;
}
Putting it all together
Adding everything together shows how useful and painless wrapping C functions in Zig can be. I added a parameter to the program allowing for a filename to be passed in order to set standard input / output to files.
// zig imports
const std = @import("std");
// c imports
const cStdio = @cImport({
@cInclude("stdio.h");
});
const cString = @cImport({
@cInclude("string.h");
});
const CStdioError = error{
printfError,
scanfError,
fgetsError,
freopenError,
};
// `printf` wrapper
fn c_printf(restrict: [*:0]const u8, args: anytype) !void {
if (@call(.auto, cStdio.printf, .{restrict} ++ args) < 1)
return CStdioError.printfError;
}
// `scanf` wrapper
fn c_scanf(restrict: [*:0]const u8, args: anytype) !void {
if (@call(.auto, cStdio.scanf, .{restrict} ++ args) < 1)
return CStdioError.scanfError;
}
// set `stdin` and `stdout` to files
fn fileIO(input: [*:0]const u8, output: [*:0]const u8) !void {
_ = cStdio.freopen(input, "r", cStdio.stdin()) orelse
return CStdioError.freopenError;
_ = cStdio.freopen(output, "w", cStdio.stdout()) orelse
return CStdioError.freopenError;
}
pub fn main() !void {
// check for `--file` flag
const flag = "--file";
const args = std.os.argv;
if (args.len > 1 and cString.strlen(flag) == cString.strlen(args[1])) {
for (flag, args[1]) |a, b|
if (a == b)
continue
else
break;
try fileIO("input.txt", "output.txt");
}
// declare variables to be read in
var i: i32 = undefined;
var s: [30]u8 = undefined;
// read `i` and `s`
try c_scanf("%d", .{&i});
try c_scanf("%s", .{&s});
// print `i` and `s`
try c_printf("%d\n", .{i});
try c_printf("%s\n", .{&s});
}