2025-02-27

vak: (Балу)
Репост от [personal profile] ufm: rant.li/johan/170-palindromov

Ленин ел
Ежу хуже
Уже лежу
Мент нем
Тип спит
Я ем змея
Лепс спел
Ох и тихо!
Тонет енот
Город дорог
+160 )
vak: (Знайка)
Есть в Rust популярная идиома: Result<(), String>. Используется для функций, которые в случае успехи ничего не возвращают, а в случае ошибки возвращают текстовую строку. К примеру, копирование одного файла в другой из реального проекта:
fn copy_file_contents(mut dest_file: &fs::File, src_filename: &str) -> Result<(), String> {
let mut src_file = fs::File::open(src_filename)
.map_err(|e| format!("Failed to open source file '{}': {}", src_filename, e))?;
io::copy(&mut src_file, &mut dest_file)
.map_err(|e| format!("Failed to copy to destination: {}", e))?;
Ok(())
}
Давайте попросим grok.com сделать аналогичный класс на Си++.

Question: There is a popular idiom: Result<(), String>. I would like to see how it could be implemented in C++.
Let’s dive into translating the Rust idiom Result<(), String> into C++. In Rust, Result is an enum used for error handling, with two variants: Ok(T) for success and Err(E) for failure. Here, Result<(), String> means a function returns either "success with no meaningful value" (Ok(()), where () is the unit type) or "failure with a string error message" (Err(String)). This is a common pattern in Rust for operations that either succeed without producing a value or fail with a descriptive error.

In C++, we don’t have a direct equivalent to Rust’s Result in the standard library, but we can approximate it using modern C++ features. The closest analogs are std::optional, std::variant, or a custom implementation.

Let’s define a reusable Result class with a Rust-like interface, using std::variant under the hood. This adds convenience methods like is_ok(), is_err(), unwrap(), and unwrap_err().
#include <variant>
#include <string>
#include <stdexcept>
#include <iostream>

template<typename T, typename E>
class Result {
public:
Result(T value) : data(value) {}
Result(E error) : data(error) {}

bool is_ok() const { return std::holds_alternative<T>(data); }
bool is_err() const { return std::holds_alternative<E>(data); }

T unwrap() const {
if (is_err()) throw std::runtime_error("Called unwrap() on an Err value");
return std::get<T>(data);
}

E unwrap_err() const {
if (is_ok()) throw std::runtime_error("Called unwrap_err() on an Ok value");
return std::get<E>(data);
}

private:
std::variant<T, E> data;
};

// Specialize for empty value and std::string
struct Void {};
using VoidResult = Result<Void, std::string>;
This version is the most Rust-like. VoidResult directly corresponds to Result<(), String>. You can call is_ok() or is_err() to check the state, and unwrap() or unwrap_err() to extract values (with error checking). It’s reusable for other T and E types too.
А вот пример использования такого класса, тоже сгенерённый Гроком.
Си++ )
Вполне разумная вещь получилась. Можно использовать в разных Си++ проектах. Полный исходник лежит здесь: result.cpp
vak: (Знайка)
Продолжим разбираться с умными указателями в Rust. Глянем на тип Rc<T>. Название расшифровывается как Reference Counted, что намекает. Спросим grok.com. Выходит почти тот же shared_ptr, с двумя отличиями: он не может оказаться NULL, и счетчик простой, а не std::atomic. Сгенерённый ним исходник смотрите в файле: rc.cpp 

Question: How Rc<T> would look if implemented in C++?

Let’s re-implement Rust’s Rc<T> in C++ - a fascinating exercise since Rc<T> introduces reference-counted shared ownership, contrasting with the unique ownership of Box<T>. In Rust, Rc<T> (short for "Reference Counted") allows multiple owners of a heap-allocated T, tracking the number of references via a counter. When the last Rc is dropped, the T is deallocated. This is akin to C++’s std::shared_ptr<T>, but we’ll craft a custom version to mirror Rust’s Rc<T> behavior, focusing on its single-threaded nature (unlike Arc<T> for multi-threading).

What Rc<T> Does in Rust

  • Allocation: Places a T on the heap with a reference count.
  • Shared Ownership: Multiple Rc<T> instances can point to the same T, incrementing the count.
  • Cloning: Rc::clone creates a new reference, bumping the count.
  • Deallocation: When the last Rc is dropped (count hits 0), T is destroyed and memory freed.
  • No Mutability: Rc<T> alone doesn’t allow mutation (use RefCell<T> or Cell<T> for that).
  • Non-Atomic: Single-threaded, unlike Arc<T>.

C++ Implementation

We’ll create an Rc<T> class in C++ that:
  • Uses a control block to store the reference count and T together.
  • Supports copying (to increment the count) and RAII cleanup.
  • Mimics Rust’s immutability by providing const access (mutation would need a separate mechanism).
  • Avoids atomic operations, matching Rc’s single-threaded design.
Here’s the code:
#include <cassert>
#include <utility>
#include <iostream>

// Control block to hold the reference count and the value
template <typename T>
struct RcControlBlock {
size_t count;
T value;

template <typename... Args>
RcControlBlock(Args&&... args) : count(1), value(std::forward<Args>(args)...) {}
};

template <typename T>
class Rc {
private:
RcControlBlock<T>* block;

// Private constructor from raw block (for internal use)
explicit Rc(RcControlBlock<T>* b) : block(b) {}

void increment() {
if (block) {
++block->count;
}
}

void decrement() {
if (block && --block->count == 0) {
delete block;
block = nullptr;
}
}

public:
// Constructor: Allocate and construct T
template <typename... Args>
explicit Rc(Args&&... args) : block(new RcControlBlock<T>(std::forward<Args>(args)...)) {}

// Destructor: Decrease count, cleanup if last reference
~Rc() {
decrement();
}

// Copy constructor: Share ownership
Rc(const Rc& other) : block(other.block) {
increment();
}

// Copy assignment: Share ownership
Rc& operator=(const Rc& other) {
if (this != &other) {
decrement(); // Drop old reference
block = other.block; // Take new reference
increment();
}
return *this;
}

// Move constructor: Transfer pointer, no count change
Rc(Rc&& other) noexcept : block(other.block) {
other.block = nullptr; // Leave moved-from empty
}

// Move assignment: Transfer pointer, no count change
Rc& operator=(Rc&& other) noexcept {
if (this != &other) {
decrement(); // Drop old reference
block = other.block; // Take new reference
other.block = nullptr; // Leave moved-from empty
}
return *this;
}

// Accessors (const only, mimicking Rust's immutability)
const T& operator*() const {
assert(block != nullptr);
return block->value;
}

const T* operator->() const {
assert(block != nullptr);
return &block->value;
}

// Clone method (like Rc::clone)
Rc clone() const {
return Rc(*this); // Uses copy constructor
}

// Get reference count (for debugging)
size_t use_count() const {
return block ? block->count : 0;
}
};

// Factory function (like Rc::new)
template <typename T, typename... Args>
Rc<T> make_rc(Args&&... args) {
return Rc<T>(std::forward<Args>(args)...);
}
This Rc<T> captures Rust’s essence - shared, immutable, single-threaded ownership.
vak: (Знайка)
Давайте выясним, чем тип Arc<T> отличается от Rc<T>. Название означает Atomically Reference Counted. Как объясняет нам grok.com, эта штука почти эквивалентна std::shared_ptr из Си++20 и позже. Сгенерённый исходник смотрите в файле: arc.cpp

Question: How Arc<T> would look if implemented in C++?

Let’s dive into how we might implement something like Rust’s Arc<T> in C++. For those unfamiliar, Arc<T> in Rust is a thread-safe, reference-counted smart pointer that allows multiple owners of some data, incrementing a counter when cloned and decrementing it when dropped, freeing the data when the count hits zero. It’s built for concurrent use, so it uses atomic operations to manage the count safely across threads. We’ll aim to replicate that behavior in C++.

In C++, we don’t have a direct equivalent in the standard library, but std::shared_ptr comes close. However, std::shared_ptr isn’t guaranteed to use atomic reference counting for the control block in all implementations (pre-C++20, at least), and its atomic operations were only standardized with std::atomic<std::shared_ptr> in C++20. To truly mirror Arc<T>, we’ll roll our own simplified version using atomic counters, focusing on thread-safety and the core mechanics.

Here’s how it could look:
#include <atomic>
#include <memory>
#include <utility>
#include <type_traits>

// Define Inner as a standalone template struct
template <typename T>
struct ArcInner {
T data;
std::atomic<int> ref_count;

template <typename... Args>
ArcInner(Args&&... args) : data(std::forward<Args>(args)...), ref_count(1) {}
};

template <typename T>
class Arc {
private:
ArcInner<T>* ptr;

void increment_count() {
if (ptr) {
ptr->ref_count.fetch_add(1, std::memory_order_relaxed);
}
}

void decrement_count() {
if (ptr) {
if (ptr->ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete ptr;
ptr = nullptr;
}
}
}

public:
// Constructor with constraint: Args must be convertible to T
template <typename... Args, typename = std::enable_if_t<std::is_constructible_v<T, Args...>>>
explicit Arc(Args&&... args) : ptr(new ArcInner<T>(std::forward<Args>(args)...)) {}

// Copy constructor
Arc(const Arc& other) : ptr(other.ptr) {
increment_count();
}

// Move constructor
Arc(Arc&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}

// Assignment operators
Arc& operator=(const Arc& other) {
if (this != &other) {
decrement_count();
ptr = other.ptr;
increment_count();
}
return *this;
}

Arc& operator=(Arc&& other) noexcept {
if (this != &other) {
decrement_count();
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}

// Destructor
~Arc() {
decrement_count();
}

// Dereference operators
T& operator*() const { return ptr->data; }
T* operator->() const { return &ptr->data; }

// Get the reference count
int use_count() const { return ptr ? ptr->ref_count.load(std::memory_order_relaxed) : 0; }
};

// Helper function to create an Arc
template <typename T, typename... Args>
Arc<T> make_arc(Args&&... args) {
return Arc<T>(std::forward<Args>(args)...);
}
vak: (Default)
Я попросил у Грока список языков программирования со статической компиляцией. Вот такая раскладка на сегодняшний момент. Весь его пространный ответ смотрите по ссылке.
  1. C++: ~20-25%
    (Still the leader due to its broad reach.)
  2. C: ~15-20%
    (Holds strong in systems programming.)
  3. Rust: ~10-13%
    (Rising fast, especially in systems and safety-critical areas.)
  4. Go: ~10-12%
    (Cloud and DevOps darling.)
  5. Swift: ~5-8%
    (Mobile heavyweight, growing beyond Apple.)
  6. Fortran: ~2-3%
    (Scientific niche.)
  7. Ada: ~1-2%
    (Safety-critical stronghold.)
  8. D: ~0.5-1%
    (Small but dedicated.)
  9. Nim: ~0.3-0.7%
    (Niche, growing slowly.)
  10. Crystal: ~0.2-0.5%
    (Ruby-inspired, limited reach.)
  11. Zig: ~0.2-0.5%
    (Emerging C alternative.)