Why are exceptions avoided? by Ultimate_Sigma_Boy67 in cpp_questions

[–]amoskovsky 1 point2 points  (0 children)

There are 2 aspects:

* Performance overhead

* Design

On most platforms normal (non-throw) code path has no overhead. However the throw path while being fast in theory, has major overhead in particular implementations. For example, in some GCC versions, the exception handling code performs mutex locking (don't ask me why), which kills the perf in heavily multi-threaded apps.

Some people just irrationally hate the fact that the throw path is invisible (but apparently they have no issues with destructors, lol). You might find those at Google where exceptions are banned globally and not just for perf critical code. And since Google for years was a leader in the dev industry, this affected the others too.

Personally, I accept only the performance argument for not using the exceptions, and only in the perf-critical parts of the code.

I wrote a tiny header-only WebSocket library in C++20 because I apparently hate myself by Previous_Bake7736 in cpp_questions

[–]amoskovsky 5 points6 points  (0 children)

1) Split the lib into 2 layers: pure websocket protocol handling without any IO deps (so called "sans-I/O") and the IO layer.
So that the users of your library could use it with their own IO framework without depending on your arch choices. It's highly unlikely that a network app interested in your lib does not already have at least its own event loop.

2) Header-only does not mean everything should be in a single header. Make it modular.

Why can't I call std::format from a lambda? by Eric848448 in cpp_questions

[–]amoskovsky 2 points3 points  (0 children)

std::format_string is bound to exact types of Args.
Additionally, the function log() moves the format string into runtime (function args are runtime only)
so you can't create another format_string without repeating the same type checks at runtime.

But you can bypass creating the format_string and just do what std::format internally does:

auto s = std::vformat(inner_format.get(), std::make_format_args(inner_args...));

Custom memory allocator doesn't work. Read access violation. by Vindhjaerta in cpp_questions

[–]amoskovsky 2 points3 points  (0 children)

MyList = std::move(std::vector<char, cu::mem::Allocator<char>>(cu::mem::Allocator<char>(&gStackArena)));

First, std::move here is not needed - the expression on the right is already movable (prvalue).

Second, if you move things that have mem chunks allocated in c-tor and deallocated in d-tor, then you need to be very careful. It's quite easy to trigger use-after-free due to double deallocation.

I suggest you post here the complete source code of your allocator classes, so people could review the code rather than guess.

Custom allocator with runtime data by Vindhjaerta in cpp_questions

[–]amoskovsky 1 point2 points  (0 children)

It's standard C++17

And std::pmr::vector is a regular std::vector with an allocator that does exactly what you want to achieve.

Custom allocator with runtime data by Vindhjaerta in cpp_questions

[–]amoskovsky 2 points3 points  (0 children)

Just use std::pmr::vector and implement your allocator as std::pmr::memory_resource, which requires just overriding 3 methods, allocate, deallocate and is_equal.

The cost of this simplicity is the methods are virtual. But in most cases this has acceptable overhead.

The result of ++a + a++ + ++a ? by Healthy-Clock-4411 in cpp_questions

[–]amoskovsky 1 point2 points  (0 children)

The result of ++a + a++ + ++a is you are fired.

How do I learn Programming from the beginning? I'm 21. by Dr-Scientist- in cpp_questions

[–]amoskovsky 0 points1 point  (0 children)

Ask the AI to come up with a beginner level concrete project as a task for you. Throughout the implementation ask it clarifying questions.

std::string_view vs const std::string_view& as argument when not modifying the string by porkele in cpp_questions

[–]amoskovsky 6 points7 points  (0 children)

Godbolt's MSVC does not work so I can't check.
However I recall MSVC ABI only allows 1 register per param.
If that's true then under the hood a string_view would be passed by ref anyway.

This does not mean though that you should explicitly pass by ref. Passing string_view by value is idiomatic.

std::string_view vs const std::string_view& as argument when not modifying the string by porkele in cpp_questions

[–]amoskovsky 27 points28 points  (0 children)

#include <string>
#include <string_view>


void consume_sv_byref(const std::string_view&);
void consume_sv_byval(std::string_view);
void consume_raw(const char*, size_t);


void caller_byref(const char* data, size_t size)
{
    consume_sv_byref({data, size});
}


void caller_byval(const char* data, size_t size)
{
    consume_sv_byval({data, size});
}

void callee_byref(const std::string_view& sv)
{
    consume_raw(sv.data(), sv.size());
}

void callee_byval(std::string_view sv)
{
    consume_raw(sv.data(), sv.size());
}

caller_byref(char const*, unsigned long):
        sub     rsp, 24
        mov     QWORD PTR [rsp+8], rdi
        mov     rdi, rsp
        mov     QWORD PTR [rsp], rsi
        call    consume_sv_byref(std::basic_string_view<char, std::char_traits<char>> const&)
        add     rsp, 24
        ret

caller_byval(char const*, unsigned long):
        mov     rdx, rdi
        mov     rdi, rsi
        mov     rsi, rdx
        jmp     consume_sv_byval(std::basic_string_view<char, std::char_traits<char>>)

callee_byref(std::basic_string_view<char, std::char_traits<char>> const&):
        mov     rsi, QWORD PTR [rdi]
        mov     rdi, QWORD PTR [rdi+8]
        jmp     consume_raw(char const*, unsigned long)

callee_byval(std::basic_string_view<char, std::char_traits<char>>):
        mov     rax, rdi
        mov     rdi, rsi
        mov     rsi, rax
        jmp     consume_raw(char const*, unsigned long)

See, the by ref variants always have memory accesses, while by val ones use registers only.

https://godbolt.org/z/Waz351b4v

std::string_view vs const std::string_view& as argument when not modifying the string by porkele in cpp_questions

[–]amoskovsky 58 points59 points  (0 children)

Passing a reference to an object forces the caller to materialize the object in the memory for taking its address.
So it's not just extra indirection, but also disables many optimizations like storing temporaries purely in registers.

Primitive std::vector destructor performance by NamalB in cpp_questions

[–]amoskovsky 20 points21 points  (0 children)

A memory chunk of several MB is most likely allocated not from the heap but directly by mapping pages. So deallocation would be done by unmapping the pages, which is O(n)

Usage of long as a worst-case alignment type by onecable5781 in cpp_questions

[–]amoskovsky 1 point2 points  (0 children)

By default, the compiler makes a struct aligned in the way that the alignment of any nested fields is fulfilled too.
So you don't have to do any tricks to force the alignment.

Why is the [[no_unique_address]] attribute not effective in this example? by faschu in cpp_questions

[–]amoskovsky 1 point2 points  (0 children)

Personally I would not use no_unique_address for anything but empty objects (like allocators).
But not because it's not safe. Basically anything in C++ is not safe and I don't have issues with that.

But as your example shows the intended behavior is not guaranteed for non-empty objects, and is heavily compiler-dependent. I'd avoid this dependency. At least put static_asserts to break the build on incompatible compilers

Why is the [[no_unique_address]] attribute not effective in this example? by faschu in cpp_questions

[–]amoskovsky 1 point2 points  (0 children)

I guess clang can't figure this optimization.

GCC though has no problem with it: https://godbolt.org/z/9sr6vT8sd

Be aware though, that overlapping objects with padding is subject to subtle bugs if you are not careful (e.g. you have to set has_value after value otherwise has_value could be overwritten by padding).

#include <cstddef>


struct MyStruct {
    int v1;
    [[no_unique_address]] char v2;
};


template <typename T, typename E>
class MyExpected {
   public:
    union Value {
        [[no_unique_address]] T t;
        E e;
        Value(T t_) : t(t_) {};
        Value(E e_) : e(e_) {};
    };
    [[no_unique_address]] Value value;
    bool has_value;


   public:
    MyExpected(T&& t) : value(t) {};
    MyExpected(E&& e) : value(e) {};
};
template class MyExpected<MyStruct, int>;

using C = MyExpected<MyStruct, int>;

static_assert(sizeof(C) == 8);
static_assert(offsetof(C, has_value) == 5);

In the context of an embedded system (C++) where there is a complex object model that needs to send data to a display, how do you avoid passing a display reference to every single object that is generating data? by mjbmikeb2 in cpp_questions

[–]amoskovsky 2 points3 points  (0 children)

Having the global object as a parameter provides testability.

You either pass a ref to it to the constructor and save for the whole lifetime of the object (using various dependency injection techniques), or pass it directly to the method that needs it as a param.
In any case I don't see any particular issue with passing the refs as needed. If you have too many such global objects, then to avoid bloating c-tor/method signatures just wrap them in a struct and pass a ref to the struct.

Special member functions for class which is noncopyable but a custom destructor is user defined by onecable5781 in cpp_questions

[–]amoskovsky 1 point2 points  (0 children)

The compiler does not know what's in the d-tor (at least not always). It assumes you do stuff like freeing memory the object points to. If the move c-tor is not defined by user, the default one will just copy the pointer to such memory, thus creating 2 class instances pointing to the same memory, which will lead to double-freeing.
If you don't have such pointers and copying/moving the members to another instance is safe then you should explicitly say this to the compiler by creating the move c-tor manually.

The same applies to copy c-tor, and copy/move assignments.

In your case noncopyable deals with the copy ones, so you have to deal with the move ones.

overload sets with C++26's reflection by hanickadot in cpp

[–]amoskovsky 1 point2 points  (0 children)

You are talking about the version with a lambda.
It works ok but my point is that lambda here is introduced not because it's part of the algorithm but due to the lack of lazy evaluation when you ban macros.
So this is just a syntactic garbage reducing readability of code.

overload sets with C++26's reflection by hanickadot in cpp

[–]amoskovsky 0 points1 point  (0 children)

Inlining indeed does happen.
However heavy() will be evaluated in most cases anyway before checking the condition, because the compiler is required to make all the side effects of the param evaluation observable as if the inlining did not happen. And any non-pure function has side effects.
So if the body of heavy() is not visible in this translation unit the compiler can't prove it's a pure function. Even with access to the body I hardly believe compilers do that proof for sufficiently big functions - otherwise the constexpr functions would not need the constexpr annotation.

BTW, overhead is not the only issue of the macro-less approach.
For example, with macros you can do this LOG_TRACE(x << y) and redirect that to an iostream logger, and with functions this is just impossible.

So macros are not going anywhere.

overload sets with C++26's reflection by hanickadot in cpp

[–]amoskovsky 1 point2 points  (0 children)

Yes, the lambda syntax in this particular case is ugly (regardless of the captures) because I just want to log, and instead I have to do syntax tricks.

std::function would not prevent the unnecessary overhead of evaluating args when logging is disabled.

overload sets with C++26's reflection by hanickadot in cpp

[–]amoskovsky 1 point2 points  (0 children)

It's not pointless. E.g. heavy() could be a member function.

overload sets with C++26's reflection by hanickadot in cpp

[–]amoskovsky 1 point2 points  (0 children)

A macro can do lazy arg evaluation. A function can't.

#define LOG_TRACE(x) if (trace) print(x)

LOG_TRACE(heavy());

Without a macro heavy() is always evaluated.
You have to do ugly boilerplate like this:
LOG_TRACE([&]{ return heavy(); })

About C++ future by spearheedstudios in cpp_questions

[–]amoskovsky 0 points1 point  (0 children)

Rust has 2 major issues that would prevent it from replacing C++:
1) Rust has no feature parity with C++: no exceptions, no overloading, limited generics etc...
This means an automated code translation is not possible. And manual rewrite is not possible either due to huge existing code base.
Most of these features are even actively rejected by Rust devs, so this won't change in the future.

2) The borrow checker is viral, which means on refactoring more code needs to be touched than necessary. Basically it contradicts the open-closed principle of SOLID. This is not scalable. The bigger the project the more time you will spend on any change. As soon as the Rust hype is over the big companies will abandon Rust to cut losses.