puts "Hello, World!" #include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
} #include <iostream> pulls in the standard I/O library. std::cout is the standard output stream; << is the stream insertion operator that sends values to it. Unlike Ruby's puts, std::cout does not add a newline automatically β you must provide std::endl (which flushes the buffer) or "\n" (which does not flush). Every C++ program starts in main(); returning 0 signals success to the OS.
# Single-line comment
count = 42 # inline
=begin
Multi-line comment
=end
puts count #include <iostream>
int main() {
// Single-line comment
int count = 42; // inline comment
/* Multi-line block comment:
inherited from C, still widely used */
std::cout << count << "\n";
return 0;
} C++ supports both // single-line comments (added in C++98) and /* ... */ block comments inherited from C. Block comments cannot be nested β a /* inside a block comment does not open a second level. The idiomatic choice is // for most in-code comments and /* ... */ for temporarily disabling large code regions.
name = "Alice"
age = 30
puts name
puts age
puts "#{name} is #{age}" #include <iostream>
#include <string>
int main() {
std::string name = "Alice";
int age = 30;
std::cout << name << "\n";
std::cout << age << "\n";
std::cout << name << " is " << age << "\n";
return 0;
} The << operator can be chained β each call returns the stream, so you can feed multiple values in one statement. There is no string interpolation syntax like Ruby's #{}; you concatenate values into the stream directly. C++20 adds std::format (covered in the Modern C++ section) for printf-style interpolation.
# Ruby: type attached to the value, not the variable
value = 42
value = "now a string" # perfectly fine
puts value #include <iostream>
#include <string>
int main() {
// C++: type is attached to the variable at declaration
int count = 42;
std::string greeting = "hello";
// count = "now a string"; // compile error: cannot assign string to int
std::cout << count << "\n";
std::cout << greeting << "\n";
return 0;
} In Ruby, a variable is just a name that can point to any object regardless of type. In C++, every variable has a fixed type declared at compile time, and the compiler rejects assignments of the wrong type. This catches an entire class of bugs before the program ever runs β at the cost of more explicit declarations.
# Ruby always infers the type from the value
count = 42
ratio = 3.14
greeting = "hello"
puts count.class # Integer
puts ratio.class # Float #include <iostream>
#include <string>
int main() {
auto count = 42; // deduced as int
auto ratio = 3.14; // deduced as double
auto greeting = std::string{"hello"}; // deduced as std::string
std::cout << count << "\n"; // 42
std::cout << ratio << "\n"; // 3.14
std::cout << greeting << "\n"; // hello
return 0;
} auto tells the compiler to deduce the variable's type from its initializer β similar to how Ruby always infers types, but in C++ the deduced type is fixed at compile time and cannot change. auto is idiomatic in modern C++ to reduce verbosity, especially with long iterator or template types. Note that auto greeting = "hello"; would deduce const char* (a C-style pointer), not std::string β the explicit std::string{} construction is required.
# Ruby unifies integers and floats under Numeric
puts 42.class # Integer
puts 3.14.class # Float
puts true.class # TrueClass
puts "hi".class # String #include <iostream>
int main() {
int count = 42;
double ratio = 3.14;
bool flag = true;
char letter = 'A';
std::cout << count << "\n"; // 42
std::cout << ratio << "\n"; // 3.14
std::cout << flag << "\n"; // 1 (true prints as 1)
std::cout << letter << "\n"; // A
return 0;
} C++ has a richer set of numeric primitives than Ruby: int (typically 32-bit), long (32 or 64-bit), long long (64-bit), float (32-bit), and double (64-bit). Ruby's Integer is arbitrary-precision; C++'s integers overflow silently. A bool prints as 1 or 0 by default β use std::boolalpha to print true/false.
SPEED_OF_LIGHT = 299_792_458
puts SPEED_OF_LIGHT
# Ruby 4.0: strings are frozen by default
greeting = "hello"
# greeting << " world" # FrozenError #include <iostream>
int main() {
const int speed_of_light = 299792458;
// speed_of_light = 0; // compile error: cannot assign to const
constexpr double pi = 3.14159265358979; // computed at compile time
std::cout << speed_of_light << "\n";
std::cout << pi << "\n";
return 0;
} const declares a variable whose value cannot be changed after initialization β the compiler rejects any attempt to assign to it. constexpr (C++11) goes further: the value must be computable at compile time, enabling the optimizer to substitute it as a literal with zero runtime cost. Prefer constexpr for true mathematical constants and const for runtime values that happen to be immutable.
# Ruby Integer is arbitrary precision β no overflow, no size choice
puts (2**62).class # Integer
puts (2**100).class # Integer (bignum, same class) #include <iostream>
#include <cstdint>
int main() {
int8_t tiny = 127;
int16_t small = 32767;
int32_t medium = 2147483647;
int64_t large = 9223372036854775807LL;
std::cout << (int)tiny << "\n"; // 127
std::cout << small << "\n"; // 32767
std::cout << medium << "\n"; // 2147483647
std::cout << large << "\n"; // 9223372036854775807
return 0;
} C++ integers have fixed sizes that vary by platform β int may be 32 or 64 bits depending on the architecture. The <cstdint> header provides types with guaranteed sizes: int32_t, uint64_t, etc. Unlike Ruby's arbitrary-precision integers, C++ integers overflow silently when they exceed their range β a common source of bugs. The int8_t value must be cast to int for printing since cout treats int8_t as a character.
greeting = "Hello"
puts greeting
puts greeting.length
puts greeting.class #include <iostream>
#include <string>
int main() {
std::string greeting = "Hello";
std::cout << greeting << "\n"; // Hello
std::cout << greeting.size() << "\n"; // 5
return 0;
} std::string is C++'s standard string class β it manages its own memory and supports all the operations you'd expect. It lives in <string>. The raw C-style string literal "Hello" is a const char*; assigning it to a std::string copies the characters into a managed heap buffer. The method .size() and .length() are identical β both return the number of bytes (not Unicode code points).
first = "Hello"
second = "World"
result = first + ", " + second + "!"
puts result #include <iostream>
#include <string>
int main() {
std::string first = "Hello";
std::string second = "World";
std::string result = first + ", " + second + "!";
std::cout << result << "\n"; // Hello, World!
return 0;
} The + operator on std::string allocates a new string containing both halves β just like Ruby. For performance-sensitive code with many concatenations, prefer += (appends in place) or std::string::append() to avoid repeated allocations. You cannot use + to concatenate two C-style string literals directly ("a" + "b" is a compile error); at least one operand must be a std::string.
name = "Alice"
age = 30
message = "#{name} is #{age} years old"
puts message #include <iostream>
#include <format>
int main() {
std::string name = "Alice";
int age = 30;
std::string message = std::format("{} is {} years old", name, age);
std::cout << message << "\n"; // Alice is 30 years old
return 0;
} std::format (C++20) is C++'s answer to Ruby's string interpolation. The {} placeholders are replaced by the arguments in order. Unlike Ruby's #{}, the format string is separate from the arguments β but it supports rich formatting: {:.2f} for floats, {:>10} for right-alignment, {:05} for zero-padding. Pre-C++20 code uses std::ostringstream or printf for the same purpose.
text = "Hello, World!"
puts text.include?("World") # true
puts text.index("World") # 7
puts text[7, 5] # World
puts text.upcase # HELLO, WORLD! #include <iostream>
#include <string>
int main() {
std::string text = "Hello, World!";
auto position = text.find("World");
std::cout << (position != std::string::npos) << "\n"; // 1 (true)
std::cout << position << "\n"; // 7
std::cout << text.substr(7, 5) << "\n"; // World
// No built-in upcase; use transform from <algorithm>
return 0;
} std::string::find() returns the index of the first match, or the sentinel std::string::npos (the largest possible size_t value) if not found β always check against npos before using the result. substr(pos, length) extracts a substring. Unlike Ruby, std::string has no built-in upcase or downcase β you use std::transform with std::toupper from <algorithm>.
puts 42.to_s # "42"
puts 3.14.to_s # "3.14"
puts Integer("42") # 42
puts Float("3.14") # 3.14 #include <iostream>
#include <string>
int main() {
std::string from_int = std::to_string(42);
std::string from_double = std::to_string(3.14);
int parsed_int = std::stoi("42");
double parsed_double = std::stod("3.14");
std::cout << from_int << "\n"; // 42
std::cout << from_double << "\n"; // 3.140000
std::cout << parsed_int << "\n"; // 42
std::cout << parsed_double << "\n"; // 3.14
return 0;
} std::to_string() converts numbers to strings; std::stoi() ("string to int"), std::stod() ("string to double"), and friends do the reverse. Note that std::to_string(3.14) produces "3.140000" with six decimal places β use std::format("{}", 3.14) (C++20) for cleaner output. The std::sto* functions throw std::invalid_argument if the string cannot be parsed.
numbers = [1, 2, 3, 4, 5]
puts numbers.length # 5
puts numbers[2] # 3
puts numbers.first # 1
puts numbers.last # 5 #include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::cout << numbers.size() << "\n"; // 5
std::cout << numbers[2] << "\n"; // 3
std::cout << numbers.front() << "\n"; // 1
std::cout << numbers.back() << "\n"; // 5
return 0;
} std::vector<T> is C++'s closest equivalent to Ruby's Array β a dynamic, resizable, contiguous sequence. The type parameter <int> is mandatory: a vector is a homogeneous collection, unlike Ruby arrays which can mix types. numbers[2] does not bounds-check (undefined behavior on out-of-bounds access); use numbers.at(2) for a checked access that throws std::out_of_range.
numbers = [1, 2, 3]
numbers.push(4)
numbers << 5
numbers.pop
puts numbers.inspect # [1, 2, 3, 4] #include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3};
numbers.push_back(4);
numbers.push_back(5);
numbers.pop_back(); // removes 5
for (int number : numbers) {
std::cout << number << " ";
}
std::cout << "\n"; // 1 2 3 4
return 0;
} push_back() appends to the end, pop_back() removes the last element (without returning it β use back() first if you need the value). To insert or remove from the middle, use insert(it, value) and erase(it) with an iterator. The range-based for loop shown here is the C++ equivalent of Ruby's each.
scores = {"Alice" => 95, "Bob" => 87, "Carol" => 92}
scores["Dave"] = 88
puts scores["Alice"] # 95
puts scores.key?("Bob") # true #include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> scores = {
{"Alice", 95}, {"Bob", 87}, {"Carol", 92}
};
scores["Dave"] = 88;
std::cout << scores["Alice"] << "\n"; // 95
std::cout << scores.count("Bob") << "\n"; // 1 (key present)
std::cout << scores.contains("Missing") << "\n"; // 0 (false)
return 0;
} std::map<K, V> is an ordered map backed by a red-black tree β iteration always yields keys in sorted order. Ruby's Hash preserves insertion order; if you need that in C++, use std::unordered_map instead (average O(1) lookup vs O(log n) for std::map). Accessing a missing key with [] silently inserts a default-constructed value β use .at(key) to throw instead, or .count(key) / .contains(key) to check existence first.
scores = {"Alice" => 95, "Bob" => 87}
scores.each do |name, score|
puts "#{name}: #{score}"
end #include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << "\n";
}
// Alice: 95
// Bob: 87
return 0;
} The C++17 structured binding const auto& [name, score] unpacks each std::pair<const K, V> entry into named variables β exactly like Ruby's block parameters in each { |name, score| }. The const auto& avoids copying the pair; omitting & would copy every entry. Because std::map is ordered, the output is alphabetical by key.
unique_tags = Set.new(["ruby", "oop", "ruby", "functional"])
unique_tags.add("concurrency")
puts unique_tags.include?("oop") # true
puts unique_tags.size # 4 #include <iostream>
#include <set>
#include <string>
int main() {
std::set<std::string> unique_tags = {"ruby", "oop", "ruby", "functional"};
unique_tags.insert("concurrency");
std::cout << unique_tags.count("oop") << "\n"; // 1 (present)
std::cout << unique_tags.size() << "\n"; // 4 (duplicate removed)
return 0;
} std::set<T> stores unique values in sorted order (backed by a red-black tree). Inserting a duplicate is silently ignored. Like std::map, it uses count() or contains() to check membership β there is no include? method. For unordered unique elements with O(1) average lookup, use std::unordered_set. Ruby's Set requires require 'set'; C++'s std::set is built into the standard library.
point = [3, 4]
puts point[0] # 3
puts point[1] # 4
triple = ["Alice", 30, true]
puts triple[0] # Alice #include <iostream>
#include <tuple>
#include <string>
int main() {
std::pair<int, int> point = {3, 4};
std::cout << point.first << "\n"; // 3
std::cout << point.second << "\n"; // 4
std::tuple<std::string, int, bool> person = {"Alice", 30, true};
std::cout << std::get<0>(person) << "\n"; // Alice
std::cout << std::get<1>(person) << "\n"; // 30
return 0;
} std::pair holds exactly two values of potentially different types, accessed via .first and .second. std::tuple generalizes this to any number of typed elements, accessed via std::get<N>() with a compile-time index. C++17 structured bindings (auto [first, second] = point;) give both types a cleaner access syntax. These types are useful for returning multiple values from a function without defining a dedicated struct.
score = 85
if score >= 90
puts "A"
elsif score >= 80
puts "B"
else
puts "C"
end #include <iostream>
int main() {
int score = 85;
if (score >= 90) {
std::cout << "A\n";
} else if (score >= 80) {
std::cout << "B\n";
} else {
std::cout << "C\n";
}
// B
return 0;
} The C++ if/else if/else chain works identically to Ruby's if/elsif/else β note the spelling difference: elsif in Ruby vs else if in C++. The condition must be in parentheses. In C++ any non-zero integer, non-null pointer, or non-empty std::optional is truthy β unlike Ruby where only nil and false are falsy.
fruits = ["apple", "banana", "cherry"]
fruits.each do |fruit|
puts fruit
end #include <iostream>
#include <vector>
#include <string>
int main() {
std::vector<std::string> fruits = {"apple", "banana", "cherry"};
for (const std::string& fruit : fruits) {
std::cout << fruit << "\n";
}
return 0;
} The range-based for loop (C++11) is C++'s equivalent of Ruby's each β it works on any type that provides begin() and end() iterators, including std::vector, std::map, arrays, and strings. The const std::string& avoids copying each element; use std::string& (without const) if you need to modify elements in place, or auto to let the compiler deduce the type.
count = 0
while count < 5
print "#{count} "
count += 1
end
puts #include <iostream>
int main() {
int count = 0;
while (count < 5) {
std::cout << count << " ";
count++;
}
std::cout << "\n"; // 0 1 2 3 4
return 0;
} The while loop is structurally identical in both languages. C++ adds the ++ and -- increment/decrement operators that Ruby lacks β they are the origin of the name "C++". C++ also has a do { ... } while (condition); variant that always executes the body at least once, and the classic for (init; condition; step) loop for index-based iteration.
direction = :north
case direction
when :north then puts "Go up"
when :south then puts "Go down"
when :east then puts "Go right"
else puts "Unknown"
end #include <iostream>
int main() {
int direction = 1; // 1=north, 2=south, 3=east
switch (direction) {
case 1: std::cout << "Go up\n"; break;
case 2: std::cout << "Go down\n"; break;
case 3: std::cout << "Go right\n"; break;
default: std::cout << "Unknown\n";
}
// Go up
return 0;
} C++'s switch only dispatches on integer values (or enums) β it cannot match strings or arbitrary objects like Ruby's case/when. Each case requires an explicit break to stop falling through to the next case; forgetting break is a common bug. C++17 adds [[fallthrough]] as an intentional fall-through annotation. For string dispatch, use an if/else if chain or a std::map<std::string, std::function<void()>>.
score = 75
grade = score >= 60 ? "pass" : "fail"
puts grade # pass #include <iostream>
int main() {
int score = 75;
std::string grade = (score >= 60) ? "pass" : "fail";
std::cout << grade << "\n"; // pass
return 0;
} The ternary operator condition ? value_if_true : value_if_false is identical in Ruby and C++. It is an expression (produces a value), not a statement, so it can appear anywhere an expression is valid β in an initializer, a function argument, or a return. The parentheses around the condition are optional but improve readability.
def add(first_number, second_number)
first_number + second_number
end
puts add(3, 4) # 7 #include <iostream>
int add(int first_number, int second_number) {
return first_number + second_number;
}
int main() {
std::cout << add(3, 4) << "\n"; // 7
return 0;
} Every C++ function must declare its return type before its name, and the types of all parameters. Unlike Ruby, which returns the value of the last expression, C++ requires an explicit return statement (except for void functions). Functions must be declared before they are used β either by defining them above main, or by providing a forward declaration (prototype) beforehand.
def greet(name, greeting = "Hello")
puts "#{greeting}, #{name}!"
end
greet("Alice") # Hello, Alice!
greet("Bob", "Hi") # Hi, Bob! #include <iostream>
#include <string>
void greet(std::string name, std::string greeting = "Hello") {
std::cout << greeting << ", " << name << "!\n";
}
int main() {
greet("Alice"); // Hello, Alice!
greet("Bob", "Hi"); // Hi, Bob!
return 0;
} Default parameter values in C++ work the same as in Ruby β omitted arguments use the default. However, C++ requires that all parameters with defaults appear after all parameters without them. There are no keyword arguments in C++ (unlike Ruby's name: syntax) β all arguments are positional. Multiple overloads (next example) are often used instead of complex default-argument combinations.
# Ruby uses duck typing instead of overloading
def describe(value)
puts value.is_a?(Integer) ? "integer: #{value}" : "string: #{value}"
end
describe(42) # integer: 42
describe("hello") # string: hello #include <iostream>
#include <string>
void describe(int value) {
std::cout << "integer: " << value << "\n";
}
void describe(std::string value) {
std::cout << "string: " << value << "\n";
}
int main() {
describe(42); // integer: 42
describe(std::string{"hello"}); // string: hello
return 0;
} C++ allows multiple functions with the same name if they differ in parameter types β the compiler selects the right one at compile time based on the argument types. Ruby achieves similar flexibility at runtime via duck typing: a single method handles many types because it calls methods on the arguments (which may or may not exist). C++ overloading is resolved at compile time with zero runtime cost; Ruby's duck typing is resolved at runtime with maximum flexibility.
def double_value(number)
number * 2 # Ruby returns a new value; original unchanged
end
count = 5
puts double_value(count) # 10
puts count # 5 (unchanged) #include <iostream>
void double_in_place(int& number) {
number *= 2; // modifies the original variable
}
int main() {
int count = 5;
double_in_place(count);
std::cout << count << "\n"; // 10 (modified in place)
return 0;
} In Ruby, integers are immutable value objects β passing one to a method cannot change the caller's variable. In C++, passing by reference (int&) gives the function a direct alias to the caller's variable, allowing in-place modification. Passing by value (int) copies the argument, leaving the caller's variable unchanged. References are also used for efficiency β passing a large std::string by const std::string& avoids copying.
def min_max(numbers)
[numbers.min, numbers.max]
end
minimum, maximum = min_max([3, 1, 4, 1, 5, 9])
puts minimum # 1
puts maximum # 9 #include <iostream>
#include <vector>
#include <algorithm>
std::pair<int, int> min_max(const std::vector<int>& numbers) {
return {*std::min_element(numbers.begin(), numbers.end()),
*std::max_element(numbers.begin(), numbers.end())};
}
int main() {
std::vector<int> numbers = {3, 1, 4, 1, 5, 9};
auto [minimum, maximum] = min_max(numbers);
std::cout << minimum << "\n"; // 1
std::cout << maximum << "\n"; // 9
return 0;
} C++ functions return exactly one value, but a std::pair or std::tuple packs multiple values into that single return. C++17 structured bindings (auto [minimum, maximum] = ...) then unpack the pair cleanly at the call site β achieving the same ergonomics as Ruby's multiple assignment. The * before std::min_element dereferences the iterator it returns into the actual value.
square = ->(x) { x * x }
puts square.call(5) # 25
puts square.(7) # 49 #include <iostream>
int main() {
auto square = [](int number) { return number * number; };
std::cout << square(5) << "\n"; // 25
std::cout << square(7) << "\n"; // 49
return 0;
} C++ lambdas use the syntax [captures](parameters) { body }. The [] is the capture list (empty here β the lambda uses no variables from the surrounding scope). The return type is deduced automatically when the body is a single return statement. Stored in an auto variable, a lambda acts just like Ruby's ->(x) { ... } lambda β it is called with () syntax.
multiplier = 3
triple = ->(x) { x * multiplier } # captures multiplier from outer scope
puts triple.call(5) # 15
puts triple.call(10) # 30 #include <iostream>
int main() {
int multiplier = 3;
auto scale = [multiplier](int number) { return number * multiplier; };
std::cout << scale(5) << "\n"; // 15
std::cout << scale(10) << "\n"; // 30
// Capture by reference: lambda sees future changes to multiplier
auto scale_ref = [&multiplier](int number) { return number * multiplier; };
multiplier = 10;
std::cout << scale_ref(5) << "\n"; // 50
return 0;
} The capture list [multiplier] captures multiplier by value β a copy is made when the lambda is created, and later changes to the original variable do not affect the lambda. Capturing by reference [&multiplier] gives the lambda a live view of the variable; modifications to either side are visible from the other. Use [=] to capture all locals by value, [&] to capture all by reference β but prefer explicit captures to make dependencies clear.
def apply(callable, value)
callable.call(value)
end
doubler = ->(x) { x * 2 }
puts apply(doubler, 21) # 42 #include <iostream>
#include <functional>
int apply(std::function<int(int)> callable, int value) {
return callable(value);
}
int main() {
std::function<int(int)> doubler = [](int number) { return number * 2; };
std::cout << apply(doubler, 21) << "\n"; // 42
// Regular functions also work as std::function arguments
std::cout << apply([](int x) { return x * x; }, 7) << "\n"; // 49
return 0;
} std::function<ReturnType(ParamTypes...)> is a type-erased wrapper that can hold any callable: a lambda, a function pointer, or a functor (an object with operator()). It is analogous to Ruby's Proc stored in a variable. The type std::function<int(int)> holds any callable that accepts an int and returns an int. For performance-critical code, prefer templated functions with auto parameters (or concepts) over std::function, which has runtime overhead from type erasure.
numbers = [5, 2, 8, 1, 9, 3]
numbers.sort_by { |n| -n }.first(3).each { |n| print "#{n} " }
puts # 9 8 5 #include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {5, 2, 8, 1, 9, 3};
std::sort(numbers.begin(), numbers.end(),
[](int first, int second) { return first > second; }); // descending
for (int i = 0; i < 3; ++i) {
std::cout << numbers[i] << " ";
}
std::cout << "\n"; // 9 8 5
return 0;
} STL algorithms like std::sort, std::find_if, and std::transform accept lambdas as their last argument, playing the role that blocks play in Ruby's sort_by, find, and map. The lambda [](int first, int second) { return first > second; } is a custom comparator that sorts in descending order. This pattern β algorithm + lambda β is the idiomatic C++ alternative to Ruby's Enumerable methods.
class Person
def initialize(name, age)
@name = name
@age = age
end
def introduce
puts "I'm #{@name}, age #{@age}"
end
end
alice = Person.new("Alice", 30)
alice.introduce #include <iostream>
#include <string>
class Person {
public:
std::string name;
int age;
Person(std::string name, int age) : name(name), age(age) {}
void introduce() {
std::cout << "I'm " << name << ", age " << age << "\n";
}
};
int main() {
Person alice("Alice", 30);
alice.introduce(); // I'm Alice, age 30
return 0;
} C++ classes use public: and private: access specifiers to control visibility β all members are private by default in a class (the opposite of struct where all members are public by default). The : name(name), age(age) after the constructor signature is a member initializer list β the idiomatic way to initialize member variables, more efficient than assignment in the constructor body. Objects are created on the stack without new.
class FileHandle
def initialize(filename)
@filename = filename
puts "Opening #{@filename}"
end
# Ruby uses GC; no guaranteed destructor
def close
puts "Closing #{@filename}"
end
end #include <iostream>
#include <string>
class FileHandle {
std::string filename;
public:
FileHandle(std::string filename) : filename(filename) {
std::cout << "Opening " << filename << "\n";
}
~FileHandle() { // destructor β runs automatically when object goes out of scope
std::cout << "Closing " << filename << "\n";
}
};
int main() {
FileHandle handle("data.txt");
// destructor runs here automatically when handle leaves scope
return 0;
} The destructor (~ClassName()) runs automatically and deterministically when an object goes out of scope β this is the foundation of RAII (Resource Acquisition Is Initialization). Unlike Ruby's garbage collector, which reclaims memory at unpredictable times, C++ destructors run at a precisely defined point. This makes C++ destructors the right place to release resources like file handles, network connections, and mutex locks.
class BankAccount
def initialize(balance)
@balance = balance
end
def balance = @balance
def deposit(amount)
@balance += amount if amount > 0
end
end
account = BankAccount.new(100)
account.deposit(50)
puts account.balance # 150 #include <iostream>
class BankAccount {
double balance; // private by default in class
public:
BankAccount(double initial) : balance(initial) {}
double get_balance() const { return balance; }
void deposit(double amount) {
if (amount > 0) balance += amount;
}
};
int main() {
BankAccount account(100.0);
account.deposit(50.0);
std::cout << account.get_balance() << "\n"; // 150
return 0;
} The const after a member function declaration means the function promises not to modify the object β calling a non-const method on a const object is a compile error. This is more explicit than Ruby's convention of naming mutating methods with !. C++ has no built-in equivalent of Ruby's attr_reader/attr_writer macros β getter and setter methods must be written explicitly.
class Animal
def initialize(name)
@name = name
end
def speak = raise NotImplementedError
def describe = puts "#{@name} says: #{speak}"
end
class Dog < Animal
def speak = "Woof!"
end
Dog.new("Rex").describe # Rex says: Woof! #include <iostream>
#include <string>
class Animal {
protected:
std::string name;
public:
Animal(std::string name) : name(name) {}
virtual std::string speak() = 0; // pure virtual (abstract)
void describe() {
std::cout << name << " says: " << speak() << "\n";
}
};
class Dog : public Animal {
public:
Dog(std::string name) : Animal(name) {}
std::string speak() override { return "Woof!"; }
};
int main() {
Dog rex("Rex");
rex.describe(); // Rex says: Woof!
return 0;
} C++ inheritance uses class Child : public Parent syntax β public inheritance preserves the parent's public interface. A virtual function enables runtime polymorphism; = 0 makes it pure virtual (abstract β must be overridden by subclasses, analogous to Ruby's raise NotImplementedError convention). The override keyword is a C++11 safety net β the compiler verifies that the function actually overrides a virtual base method. The protected: specifier allows subclasses to access name while hiding it from external code.
class Shape
def area = raise NotImplementedError
end
class Circle < Shape
def initialize(radius) = @radius = radius
def area = Math::PI * @radius ** 2
end
class Square < Shape
def initialize(side) = @side = side
def area = @side ** 2
end
shapes = [Circle.new(3), Square.new(4)]
shapes.each { |shape| puts shape.area.round(2) } #include <iostream>
#include <vector>
#include <memory>
#include <cmath>
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
Circle(double radius) : radius(radius) {}
double area() const override { return M_PI * radius * radius; }
};
class Square : public Shape {
double side;
public:
Square(double side) : side(side) {}
double area() const override { return side * side; }
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(3.0));
shapes.push_back(std::make_unique<Square>(4.0));
for (const auto& shape : shapes) {
std::cout << shape->area() << "\n";
}
// 28.2743
// 16
return 0;
} C++ polymorphism requires pointers or references β calling a virtual method on a value type uses the static (compile-time) type, not the runtime type. std::unique_ptr (covered in the Memory section) manages the heap-allocated objects automatically. The virtual ~Shape() = default virtual destructor is essential: without it, deleting a Circle via a Shape* would only call Shape's destructor, leaking memory. The -> operator dereferences a pointer and accesses a member in one step.
class Vector2D
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def +(other) = Vector2D.new(@x + other.x, @y + other.y)
def to_s = "(#{@x}, #{@y})"
end
result = Vector2D.new(1, 2) + Vector2D.new(3, 4)
puts result # (4, 6) #include <iostream>
struct Vector2D {
double x, y;
Vector2D(double x, double y) : x(x), y(y) {}
Vector2D operator+(const Vector2D& other) const {
return {x + other.x, y + other.y};
}
};
std::ostream& operator<<(std::ostream& stream, const Vector2D& vector) {
return stream << "(" << vector.x << ", " << vector.y << ")";
}
int main() {
Vector2D result = Vector2D(1, 2) + Vector2D(3, 4);
std::cout << result << "\n"; // (4, 6)
return 0;
} C++ allows overloading almost any operator β +, -, *, [], (), <<, and more. Ruby similarly allows defining +, [], and other operators as methods. Overloading << for std::ostream is the C++ equivalent of Ruby's to_s β it tells cout how to print the object. A struct is identical to a class except that members default to public.
# Ruby uses duck typing β one method works for any type
def largest(first, second)
first > second ? first : second
end
puts largest(3, 7) # 7
puts largest("apple", "mango") # mango #include <iostream>
#include <string>
template<typename T>
T largest(T first, T second) {
return first > second ? first : second;
}
int main() {
std::cout << largest(3, 7) << "\n"; // 7
std::cout << largest(std::string{"apple"}, std::string{"mango"}) << "\n"; // mango
std::cout << largest(3.14, 2.72) << "\n"; // 3.14
return 0;
} Templates achieve what Ruby achieves through duck typing β a single definition that works for any type β but the mechanism is different: the compiler generates a separate specialized version of the function for each type used. This is resolved at compile time with zero runtime overhead. The typename T declares a type parameter. C++20 adds auto parameters and concepts (type constraints) for cleaner template definitions: auto largest(auto first, auto second) works in C++20.
# Ruby: a single Stack class works for any type
class Stack
def initialize = @items = []
def push(item) = @items.push(item)
def pop = @items.pop
def top = @items.last
def empty? = @items.empty?
end
stack = Stack.new
stack.push(42)
puts stack.top # 42 #include <iostream>
#include <vector>
template<typename T>
class Stack {
std::vector<T> items;
public:
void push(T item) { items.push_back(item); }
void pop() { items.pop_back(); }
T top() const { return items.back(); }
bool empty() const { return items.empty(); }
};
int main() {
Stack<int> integer_stack;
integer_stack.push(42);
std::cout << integer_stack.top() << "\n"; // 42
Stack<std::string> string_stack;
string_stack.push("hello");
std::cout << string_stack.top() << "\n"; // hello
return 0;
} A class template parameterizes an entire class over a type. Stack<int> and Stack<std::string> are two different classes generated from the same template definition. The STL containers (std::vector<T>, std::map<K, V>) are all class templates. Unlike Ruby's single Array class that holds any object, each std::vector instantiation holds exactly one type β enforced at compile time.
# Ruby has no compile-time type constraints;
# if the type doesn't support the operation, it fails at runtime
def add(first, second)
first + second # works for Numeric, String, Array...
end
puts add(3, 4) # 7
puts add("a", "b") # ab #include <iostream>
#include <concepts>
template<typename T>
requires std::integral<T> || std::floating_point<T>
T add(T first, T second) {
return first + second;
}
int main() {
std::cout << add(3, 4) << "\n"; // 7
std::cout << add(1.5, 2.5) << "\n"; // 4
// add(std::string{"a"}, std::string{"b"}); // compile error: constraint not satisfied
return 0;
} C++20 concepts let you express type requirements on template parameters that the compiler verifies at the call site, producing clear error messages instead of cryptic template expansion errors. std::integral and std::floating_point are standard concepts from <concepts>. You can define your own: template<typename T> concept Printable = requires(T t) { std::cout << t; };. This is the C++ equivalent of Ruby's duck-typing checks, made explicit and compiler-enforced.
# Ruby allocates all objects on the heap, managed by GC
person = {name: "Alice", age: 30}
numbers = [1, 2, 3]
# GC decides when to free these #include <iostream>
#include <string>
#include <vector>
int main() {
// Stack allocation β automatic, freed when scope ends
int count = 42;
std::string greeting = "hello"; // std::string manages its own heap buffer internally
// Heap allocation β use smart pointers, not raw new/delete
auto numbers = std::vector<int>{1, 2, 3}; // vector manages heap memory internally
std::cout << count << "\n"; // 42
std::cout << greeting << "\n"; // hello
std::cout << numbers[0] << "\n"; // 1
return 0;
// All stack objects are automatically destroyed here
} In Ruby every object lives on the garbage-collected heap. In C++, local variables live on the stack and are freed automatically and deterministically when they leave scope β no GC required. Types like std::string and std::vector internally manage heap memory for their data, but their ownership bookkeeping lives on the stack and is released via their destructors. Modern C++ avoids raw new/delete entirely; use stack allocation and smart pointers instead.
# Ruby GC handles ownership automatically
class Resource
def initialize(name) = puts "Acquired: #{name}"
def use = puts "Using resource"
end
resource = Resource.new("database connection")
resource.use
# resource freed when GC runs (non-deterministic) #include <iostream>
#include <memory>
#include <string>
class Resource {
std::string name;
public:
Resource(std::string name) : name(name) {
std::cout << "Acquired: " << name << "\n";
}
~Resource() {
std::cout << "Released: " << name << "\n";
}
void use() { std::cout << "Using " << name << "\n"; }
};
int main() {
auto resource = std::make_unique<Resource>("database connection");
resource->use();
return 0;
// Resource released automatically here β destructor runs, output: "Released: database connection"
} std::unique_ptr<T> (C++11) is a smart pointer that owns its object exclusively β when the unique_ptr is destroyed (goes out of scope, or is explicitly reset), it automatically deletes the managed object. There can be only one unique_ptr to any given object; ownership can be transferred with std::move but not copied. Use std::make_unique<T>(args) rather than new directly. Access members via -> just like a raw pointer.
# Ruby objects are reference-counted internally
data = [1, 2, 3]
alias_ref = data # both point to the same array
alias_ref << 4
puts data.inspect # [1, 2, 3, 4]
# freed when all references go out of scope #include <iostream>
#include <memory>
#include <vector>
int main() {
auto data = std::make_shared<std::vector<int>>(std::initializer_list<int>{1, 2, 3});
auto alias_ref = data; // shared ownership; reference count is now 2
alias_ref->push_back(4);
for (int number : *data) {
std::cout << number << " ";
}
std::cout << "\n"; // 1 2 3 4
std::cout << "use_count: " << data.use_count() << "\n"; // 2
return 0;
// Both unique_ptrs go out of scope; vector freed when count reaches 0
} std::shared_ptr<T> uses reference counting β the managed object is deleted only when the last shared_ptr pointing to it is destroyed. This is similar to Ruby's internal reference counting for garbage collection. Use shared_ptr when multiple parts of the program share ownership of the same object; use unique_ptr when ownership is clear and singular. Prefer unique_ptr by default β the explicit transfer of ownership via std::move makes code intent clearer.
# Ruby uses blocks for resource management
File.open("data.txt") do |file|
puts file.read
end # file closed automatically when block exits #include <iostream>
#include <fstream>
#include <string>
int main() {
{ // inner scope β file handle lives only here
std::ifstream file_handle("data.txt");
if (file_handle.is_open()) {
std::string line;
std::getline(file_handle, line);
std::cout << line << "\n";
}
} // file_handle destructor runs here β file closed automatically
// Cannot accidentally use file_handle here β it's out of scope
return 0;
} RAII (Resource Acquisition Is Initialization) means tying a resource's lifetime to an object's lifetime β acquire in the constructor, release in the destructor. When the object goes out of scope, the destructor runs and the resource is freed, even if an exception is thrown. Ruby achieves the same pattern with blocks (File.open { |f| ... }), but C++ RAII is automatic for any stack-allocated object. The file in this example is marked norun because Compiler Explorer restricts filesystem access.
def divide(dividend, divisor)
raise ArgumentError, "Cannot divide by zero" if divisor == 0
dividend / divisor
end
begin
puts divide(10, 2) # 5
puts divide(10, 0) # raises
rescue ArgumentError => error
puts "Error: #{error.message}"
end #include <iostream>
#include <stdexcept>
double divide(double dividend, double divisor) {
if (divisor == 0) {
throw std::invalid_argument("Cannot divide by zero");
}
return dividend / divisor;
}
int main() {
try {
std::cout << divide(10.0, 2.0) << "\n"; // 5
std::cout << divide(10.0, 0.0) << "\n"; // throws
} catch (const std::invalid_argument& error) {
std::cout << "Error: " << error.what() << "\n";
}
return 0;
} The C++ exception model maps cleanly to Ruby's: throw β raise, try { ... } catch (...) { ... } β begin/rescue/end. The standard exception hierarchy lives in <stdexcept>: std::runtime_error, std::invalid_argument, std::out_of_range, etc. β analogous to Ruby's StandardError hierarchy. Catching by const& avoids copying. Unlike Ruby, C++ exceptions are not used for flow control in performance-sensitive code β they are slower than return values when the error path is frequently taken.
begin
Integer("not a number")
rescue ArgumentError => error
puts "Argument error: #{error.message}"
rescue TypeError => error
puts "Type error: #{error.message}"
rescue => error
puts "Other error: #{error.message}"
end #include <iostream>
#include <stdexcept>
void risky_operation(int choice) {
if (choice == 1) throw std::invalid_argument("bad argument");
if (choice == 2) throw std::runtime_error("runtime failure");
if (choice == 3) throw 42; // throwing a non-exception type
}
int main() {
for (int choice = 1; choice <= 3; ++choice) {
try {
risky_operation(choice);
} catch (const std::invalid_argument& error) {
std::cout << "Argument error: " << error.what() << "\n";
} catch (const std::runtime_error& error) {
std::cout << "Runtime error: " << error.what() << "\n";
} catch (...) {
std::cout << "Unknown error\n";
}
}
return 0;
} Multiple catch clauses are tried in order β the first matching type is used, just like Ruby's multiple rescue clauses. The catch-all catch (...) matches any thrown value, including non-exception types (C++ allows throwing integers or strings, though this is considered bad practice). In Ruby, any object can be raised with raise; in C++, best practice is to always throw objects derived from std::exception so catch (const std::exception& e) works as a safe catch-all.
def find_user(user_id)
users = {1 => "Alice", 2 => "Bob"}
users[user_id] # returns nil if not found
end
result = find_user(1)
puts result ? "Found: #{result}" : "Not found"
result = find_user(99)
puts result ? "Found: #{result}" : "Not found" #include <iostream>
#include <optional>
#include <string>
#include <map>
std::optional<std::string> find_user(int user_id) {
std::map<int, std::string> users = {{1, "Alice"}, {2, "Bob"}};
auto it = users.find(user_id);
if (it != users.end()) return it->second;
return std::nullopt; // like Ruby's nil
}
int main() {
auto result = find_user(1);
std::cout << (result ? "Found: " + *result : "Not found") << "\n"; // Found: Alice
result = find_user(99);
std::cout << (result ? "Found: " + *result : "Not found") << "\n"; // Not found
return 0;
} std::optional<T> (C++17) represents a value that may or may not be present β a type-safe alternative to returning a sentinel value or null pointer. It plays the role of Ruby's nil for functions that may fail to find a result. Check for a value with if (result) or result.has_value(); access the value with *result or result.value() (which throws std::bad_optional_access if empty). std::nullopt is the "empty optional" constant, analogous to nil.
numbers = [5, 2, 8, 1, 9, 3]
puts numbers.sort.inspect # [1, 2, 3, 5, 8, 9]
puts numbers.sort { |a, b| b <=> a }.inspect # [9, 8, 5, 3, 2, 1] #include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {5, 2, 8, 1, 9, 3};
std::sort(numbers.begin(), numbers.end()); // ascending
for (int n : numbers) std::cout << n << " ";
std::cout << "\n"; // 1 2 3 5 8 9
std::sort(numbers.begin(), numbers.end(),
[](int first, int second) { return first > second; }); // descending
for (int n : numbers) std::cout << n << " ";
std::cout << "\n"; // 9 8 5 3 2 1
return 0;
} std::sort sorts a range defined by two iterators β begin() and end() β in place. Unlike Ruby's sort (which returns a new array), std::sort always modifies the collection in place (like Ruby's sort!). The optional third argument is a custom comparator lambda. C++20 ranges allow std::ranges::sort(numbers) β shorter and equivalent.
numbers = [1, 4, 7, 10, 13]
even = numbers.find { |n| n.even? }
puts even # 4 #include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 4, 7, 10, 13};
auto iterator = std::find_if(numbers.begin(), numbers.end(),
[](int number) { return number % 2 == 0; });
if (iterator != numbers.end()) {
std::cout << *iterator << "\n"; // 4
}
return 0;
} std::find_if returns an iterator to the first element matching the predicate β not the element itself. To get the value, dereference it with *iterator. Always check against end() before dereferencing; dereferencing an end iterator is undefined behavior. This is analogous to Ruby's find/detect, which returns nil when nothing matches. C++23 adds std::ranges::find_if for a cleaner API.
numbers = [1, 2, 3, 4, 5]
squares = numbers.map { |n| n * n }
puts squares.inspect # [1, 4, 9, 16, 25] #include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> squares(numbers.size());
std::transform(numbers.begin(), numbers.end(), squares.begin(),
[](int number) { return number * number; });
for (int square : squares) std::cout << square << " ";
std::cout << "\n"; // 1 4 9 16 25
return 0;
} std::transform applies a function to each element and writes the results to an output iterator β the C++ equivalent of Ruby's map. Unlike map, it writes into a pre-allocated destination (here squares, sized to match numbers); it does not return a new collection. C++20 ranges offer std::views::transform(numbers, fn) as a lazy, non-allocating alternative that is more composable.
numbers = [1, 2, 3, 4, 5]
puts numbers.sum # 15
puts numbers.reduce(:*) # 120 (product) #include <iostream>
#include <vector>
#include <numeric>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int total = std::accumulate(numbers.begin(), numbers.end(), 0);
int product = std::accumulate(numbers.begin(), numbers.end(), 1,
[](int running, int number) { return running * number; });
std::cout << total << "\n"; // 15
std::cout << product << "\n"; // 120
return 0;
} std::accumulate from <numeric> folds a range into a single value, starting from an initial value β equivalent to Ruby's reduce/inject. The third argument is the starting value (0 for sum, 1 for product). The optional fourth argument is a custom binary operation. C++17 adds std::reduce (same as accumulate but parallelizable) and std::inclusive_scan/exclusive_scan for running totals.
numbers = [1, 2, 3, 4, 5, 6, 7, 8]
evens = numbers.select { |n| n.even? }
puts evens.inspect # [2, 4, 6, 8] #include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8};
std::vector<int> evens;
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(evens),
[](int number) { return number % 2 == 0; });
for (int even : evens) std::cout << even << " ";
std::cout << "\n"; // 2 4 6 8
return 0;
} std::copy_if is the C++ equivalent of Ruby's select/filter. std::back_inserter(evens) is an output iterator that calls push_back on evens for each element that passes the predicate β it grows the destination vector automatically. The C++20 ranges version is more concise: auto evens = numbers | std::views::filter([](int n){ return n % 2 == 0; });.
numbers = [2, 4, 6, 8]
puts numbers.all?(&:even?) # true
puts numbers.any? { |n| n > 5 } # true
puts numbers.none?(&:odd?) # true #include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {2, 4, 6, 8};
bool all_even = std::all_of(numbers.begin(), numbers.end(),
[](int n) { return n % 2 == 0; });
bool any_large = std::any_of(numbers.begin(), numbers.end(),
[](int n) { return n > 5; });
bool none_odd = std::none_of(numbers.begin(), numbers.end(),
[](int n) { return n % 2 != 0; });
std::cout << all_even << "\n"; // 1 (true)
std::cout << any_large << "\n"; // 1 (true)
std::cout << none_odd << "\n"; // 1 (true)
return 0;
} std::all_of, std::any_of, and std::none_of are direct counterparts to Ruby's all?, any?, and none?. They short-circuit: all_of stops at the first failing element, any_of stops at the first passing element. All three print 1 for true and 0 for false β use std::boolalpha to print true/false instead.
person = ["Alice", 30, "engineer"]
name, age, role = person
puts "#{name} (#{age}) β #{role}" #include <iostream>
#include <tuple>
#include <string>
#include <map>
int main() {
// Destructuring a tuple
auto person = std::tuple{"Alice", 30, std::string{"engineer"}};
auto [name, age, role] = person;
std::cout << name << " (" << age << ") β " << role << "\n"; // Alice (30) β engineer
// Destructuring a map entry
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto& [player, score] : scores) {
std::cout << player << ": " << score << "\n";
}
return 0;
} Structured bindings (auto [a, b, c] = ..., C++17) let you unpack tuples, pairs, arrays, and structs into named variables β exactly like Ruby's multiple assignment. They work in for loops to destructure map entries, and with function return values of type std::pair or std::tuple. The binding const auto& avoids copying; auto alone copies the element.
# Ruby array literal syntax
numbers = [1, 2, 3, 4, 5]
puts numbers.sum # 15
greeting = "Hello"
puts greeting #include <iostream>
#include <vector>
#include <string>
#include <numeric>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5}; // brace-initialization
std::cout << std::accumulate(numbers.begin(), numbers.end(), 0) << "\n"; // 15
std::string greeting{"Hello"}; // also brace-initialization
std::cout << greeting << "\n"; // Hello
return 0;
} Brace initialization ({...}, C++11) is the modern, preferred way to initialize objects in C++ β it works uniformly for arrays, containers, structs, and class objects, and prevents narrowing conversions. int count{42} is equivalent to int count = 42 but would reject int count{3.14} (whereas int count = 3.14 silently truncates). The = {1, 2, 3} syntax on a std::vector invokes its std::initializer_list constructor.
module Direction
NORTH = :north
SOUTH = :south
EAST = :east
WEST = :west
end
direction = Direction::NORTH
puts direction == Direction::NORTH # true #include <iostream>
enum class Direction { North, South, East, West };
std::string direction_name(Direction direction) {
switch (direction) {
case Direction::North: return "North";
case Direction::South: return "South";
case Direction::East: return "East";
case Direction::West: return "West";
}
return "Unknown";
}
int main() {
Direction direction = Direction::North;
std::cout << direction_name(direction) << "\n"; // North
std::cout << (direction == Direction::North) << "\n"; // 1 (true)
return 0;
} enum class (C++11) creates a strongly-typed, scoped enum β values must be qualified with the enum name (Direction::North, not just North), and they cannot be implicitly converted to integers. This is safer than the old C-style enum, which polluted the enclosing namespace. The concept is similar to Ruby module constants (Direction::NORTH) but enforced by the compiler's type system β you cannot accidentally compare a Direction with a Color.
# Ruby evaluates everything at runtime
def factorial(n)
n <= 1 ? 1 : n * factorial(n - 1)
end
puts factorial(10) # 3628800 #include <iostream>
constexpr long long factorial(int number) {
return number <= 1 ? 1 : number * factorial(number - 1);
}
int main() {
// Evaluated at compile time β zero runtime cost
constexpr long long result = factorial(10);
std::cout << result << "\n"; // 3628800
// Also works at runtime with a non-constexpr argument
int dynamic_input = 7;
std::cout << factorial(dynamic_input) << "\n"; // 5040
return 0;
} constexpr functions (C++11) can be evaluated at compile time when all their arguments are compile-time constants, embedding the result directly in the binary with zero runtime overhead. The same function also works at runtime when called with runtime values. This is a superpower Ruby lacks entirely β Ruby evaluates all code at runtime. constexpr is widely used for mathematical constants, lookup table generation, and compile-time validation.
numbers = (1..10).to_a
result = numbers.select { |n| n.even? }.map { |n| n * n }.first(3)
puts result.inspect # [4, 16, 36] #include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto pipeline = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(3);
for (int result : pipeline) {
std::cout << result << " ";
}
std::cout << "\n"; // 4 16 36
return 0;
} C++20 ranges introduce a pipeline syntax using | that composes lazily β no intermediate containers are allocated. This is the C++ equivalent of Ruby's chained Enumerable methods (select.map.first). The std::views::filter, std::views::transform, and std::views::take adaptors create lazy views that only compute values as they are consumed by the for loop. This lazy evaluation is similar to Ruby's lazy enumerator.
printf "%-10s %5.2f\n", "pi", 3.14159
printf "%-10s %5d\n", "count", 42 #include <iostream>
#include <format>
int main() {
// Width, alignment, and precision
std::cout << std::format("{:<10} {:>5.2f}\n", "pi", 3.14159); // pi 3.14
std::cout << std::format("{:<10} {:>5}\n", "count", 42); // count 42
std::cout << std::format("Hex: {:#x} Oct: {:#o}\n", 255, 255);
// Hex: 0xff Oct: 0377
return 0;
} std::format (C++20) uses a mini-language for format specifiers: {:<10} is left-aligned in a 10-character field, {:>5.2f} is right-aligned with 2 decimal places, {:#x} is hexadecimal with 0x prefix. The syntax is inspired by Python's str.format() and is considerably more powerful than printf. A companion std::println (C++23) adds the newline automatically.
module Geometry
module Shapes
class Circle
def initialize(radius) = @radius = radius
def area = Math::PI * @radius ** 2
end
end
end
circle = Geometry::Shapes::Circle.new(5)
puts circle.area.round(2) # 78.54 #include <iostream>
#include <cmath>
namespace Geometry {
namespace Shapes {
struct Circle {
double radius;
Circle(double radius) : radius(radius) {}
double area() const { return M_PI * radius * radius; }
};
}
}
int main() {
Geometry::Shapes::Circle circle(5.0);
std::cout << circle.area() << "\n"; // 78.5398
// C++17 nested namespace syntax
// namespace Geometry::Shapes { ... } β same as above, shorter
return 0;
} C++ namespaces play the same organizational role as Ruby's modules: they prevent name collisions and group related declarations. The :: scope resolution operator is the same as Ruby's. C++17 allows the compact form namespace A::B::C { ... } instead of three nested namespace blocks. The often-seen using namespace std; brings all of std into scope β convenient but considered poor practice in headers, since it can cause unexpected name collisions.