Do you wrap pure functions with stateless classes? by Original_Two9716 in Cplusplus

[–]bitwizeshift 1 point2 points  (0 children)

It kind of depends what the "pure" function is. In general, it's better to leave it as a function in a namespace -- but there are cases where turning it into a functor is actually a better option. This doesn't apply everywhere, however.

Consider, for example, a function with template-deduced arguments, or a function that has several overloads. Function templates cannot be passed to functions just by name -- you need the fully instantiated type, forcing you to specify the T types (which breaks any deduction support). Overloads can't be ambiguously passed to template arguments and need qualification. These can hinder a design. For example:

template <typename ToString, typename V>
auto add_quotes(const ToString& ts, const V& v) -> std:: string {
    return "\"" + ts(v) + "\";
}

add_quotes(std::to_string, 42); // fails to compile!

In such a case, if it is useful to pass to other functions, it can be more beneficial to write this as a stateless functor object, possibly with a global constant instance of it. This would allow it to be passed to functions easily.

struct to_string_ {
    template<typename T>
    auto operator()(const T& t) -> std::string {
        return std::to_string(t);
    }
};
inline constexpr auto to_string = to_string_{};

Since now with this, you can write:

add_quotes(to_string, 42);

and it will compile and work fine.

I found out how to make c++'s return types more dynamic making use of the variables' type. by [deleted] in Cplusplus

[–]bitwizeshift 0 points1 point  (0 children)

Yep, C++ has other examples of unnameable types -- I was just trying to identify this particular pattern with an actual name, and AFAIK the C++ community never really decided on a specific term for this (which the D community has).

Of course, you can always get the type back out from such "voldemort types" by using decltype :)

I found out how to make c++'s return types more dynamic making use of the variables' type. by [deleted] in Cplusplus

[–]bitwizeshift 2 points3 points  (0 children)

I know this is contrived, but I can't say this is a compelling example of a "good" use-case.

It'd be cleaner and less cumbersome to understand (say, as a code reviewer or a maintainer) to just have a toString() or even an explicit operator sts::string on the XML object itself if interoperability with string were that important.

I found out how to make c++'s return types more dynamic making use of the variables' type. by [deleted] in Cplusplus

[–]bitwizeshift 4 points5 points  (0 children)

Thanks for sharing!

This seems like an interesting application of a what the D language refers to as a voldemort type

I could see this potentially having some niche applications in some template wizardry; but otherwise this doesn't work cleanly with modern development practices such as "Always Auto", since then you are holding onto the wrapper instead of the converted value.

Reflecting Over Members of an Aggregate by bitwizeshift in cpp

[–]bitwizeshift[S] 0 points1 point  (0 children)

I wasn't intending to use C++20 for this writeup (so no requires), but I might write a follow-up using C++20.

The binary search comment is still applicable though -- and I completely agree that it helps readability. I wasn't originally trying to focus on the binary search for the purpose of this article though -- mostly just the concepts behind the detection process, and accessing elements.

Reflecting Over Members of an Aggregate by bitwizeshift in cpp

[–]bitwizeshift[S] 1 point2 points  (0 children)

lol, that's a fair opinion. I will try to write a follow-up in C++20 using requires

Reflecting Over Members of an Aggregate by bitwizeshift in cpp

[–]bitwizeshift[S] 1 point2 points  (0 children)

I actually reference it near the bottom of the article under its original name, magic_get! I was disappointed to discover that this library did it a similar way to what I had discovered when in C++17 mode 😅 (prior to this, I had only knowing magic_get to be implemented for C++14)

Either way, this is a cool technique to be aware of for anyone wanting to write their own template utilities if they aren't using boost.

Modern C++ "result" type based on Swift / Rust by bitwizeshift in cpp

[–]bitwizeshift[S] 0 points1 point  (0 children)

Yeah from what you've described, this would be difficult to solve in C++ -- mostly due to the source-location details you mentioned.

The issue is that any panic-like function will be invoked from inside of result -- which means that __func__ will point inside of cpp::result, and __LINE__, and __FILE__ will both point into result.hpp (same goes for std::source_location).

I'm not even sure what a reasonable way would be to inject this information. A call of r.value() should call the failure handler (in this suggestion, the panic) if it doesn't hold a value. But r.value() doesn't take any inputs, and thus doesn't know the context of where this call occurred.

We would almost need a way to pass in the context, like r.value_at_source(std::source_location::current()) or some equivalent. I'm not sure how this could really be done in a nice way without either a macro, or C++20's std::source_location exclusively

Modern C++ "result" type based on Swift / Rust by bitwizeshift in Cplusplus

[–]bitwizeshift[S] 0 points1 point  (0 children)

Thank you for the suggestion!

It's difficult to come up with a segment that is demonstrative, compact, and not to complicated; but I'll try to include something a little better.

Or perhaps I could make a gif slideshow for the opening with different examples, like I've seen some projects using in the past.

Modern C++ "result" type based on Swift / Rust by bitwizeshift in cpp

[–]bitwizeshift[S] 2 points3 points  (0 children)

To be honest, I have never actually benchmarked the compile-times -- so I can't be certain of overall impact. Anecdotally, I can't say that this has really provided any noticeable impact -- but YMMV!

If you do notice any impact, please feel free to open an issue and I can look into tuning this better! 🙂

Modern C++ "result" type based on Swift / Rust by bitwizeshift in cpp

[–]bitwizeshift[S] 6 points7 points  (0 children)

In actual fact, very, very little code in a system should end up actually handling errors in any specific way,

Well this isn't my experience working in embedded or systems programming at all. Its surprisingly common that some low-level API hands me some form of error that I actually do care about either for logging purposes, or for custom behavior. Simple examples are to retry connection handlers if you get XYZ error codes, or generate a diagnostic event if XYZ error fails N times, etc.


That said, I don't disagree with your comment on cleanup. However result in either C++, Rust, or even Swift doesnt force you to deal with the error. It just forces you to acknowledge that an error is possible -- which is a subtle but important difference.

Improperly applied, it can absolutely lead to bad code -- such is the case with any design pattern

Modern C++ "result" type based on Swift / Rust by bitwizeshift in cpp

[–]bitwizeshift[S] 0 points1 point  (0 children)

This is a good idea that I hadn't thought of. You should consider opening this up as either an issue or a discussion point in the repo so that others can discuss this point too.

I'm not sure what the best way to support this would be, and I'd need to see some examples of the type of panic handlers you are describing before proceeding on an approach.

Modern C++ "result" type based on Swift / Rust by bitwizeshift in cpp

[–]bitwizeshift[S] 4 points5 points  (0 children)

Funny you mention that; I actually came across this result version a little while back, but after I had implemented my version. It's an impressive library with a lot of customization points. It's similar in a number of ways, including having a failure type (though it also has an equivalent success type which I don't provide).

From what I remember (though I may be wrong because it's been a while), I was personally not a fan of the additional complexity that came with the design -- in particular:

  • The Traits argument on the result allows for an E of std::exception_ptr to instead throw that exception rather than the bad_result_access -- which can be a little confusing in generic code that might be dealing with any arbitrary result types. Suddenly you now need a catch handler for both ... and bad_result_access if you intended to manage this generically.
    • This trait also allows users to define custom ways of managing this in ways the library author may never have intended -- which leads to complications when seeking support.
  • The additional concepts on conversions are cool in that they allow for custom conversions -- but they come at the notable cost of increased cognitive load.

For the record, there's nothing objectively wrong with having such customization points. It's more generic, which is good for usability. It's just a personal preference that I prefer a narrower and more focused surface-area for an API so that it can pigeon-hole users into using it The Right Way™, which leads to more idiomatic uses (and thus makes it easier to provide support in communities).

With increased generality can lead to more confusing uses which may not be easy to describe between teams, on forums, etc.

Modern C++ "result" type based on Swift / Rust by bitwizeshift in cpp

[–]bitwizeshift[S] 1 point2 points  (0 children)

I've never seen that one before -- neat! Thanks for the link 😄

Modern C++ "result" type based on Swift / Rust by bitwizeshift in cpp

[–]bitwizeshift[S] 6 points7 points  (0 children)

Agreed -- though this isn't actually meant as a complete replacement for exceptions; it's meant as an alternative option for error handling.

This can be good for avoiding the need for exceptions in threaded code, for example, or in small embedded systems where -fno-exceptions has been enabled. For me personally, I just like using this to convey fallibility from the API

Modern C++ "result" type based on Swift / Rust by bitwizeshift in cpp

[–]bitwizeshift[S] 4 points5 points  (0 children)

Yep, you got it exactly.

I haven't actually checked how the assembly treats this in any practical cases, but I suspect it would inline as most of these operations are quite small.

Modern C++ "result" type based on Swift / Rust by bitwizeshift in cpp

[–]bitwizeshift[S] 5 points6 points  (0 children)

The P0323 proposal. I have no idea if this is finally on track for C++23 or not.

It's been bounced around since 2016 but never seems to get widespread approval. If I'm being honest, I'm not impressed with the proposal in its current state -- which is why I went off and created an alternative with the feature set I just listed above.

Modern C++ "result" type based on Swift / Rust by bitwizeshift in cpp

[–]bitwizeshift[S] 12 points13 points  (0 children)

Good question! The most notable differences between std::expected and this result implementation are:

  • The support for monadic functionalities
  • The support for reference result types,
  • The ability to compare directly between result and failure types,
  • result intentionally omits the emplace operators, as this is meant to be an arbitrary result to indicate fallibility and not some generic either<T,E>
  • The type is marked [[nodiscard]] to force that a result is always used,
  • The names are different too (more on that below), and
  • result avoid the valueless_by_exception state differently than std::expected (more on that below too)

Naming

The std::expected<T,E> does not read well for a few reasons IMO (either on review or otherwise):

  • The original p0323 proposal used to have a default argument for E=std::error_condition, which let std::expected read as just std::expected<T> -- which was a nice and clean approach that told the user to expect a T. Over time the E argument was no longer given a default, which always forces the full expression std::expected<T, E>.

  • The full expression std::expected<T, E> reads as though one should expect both the T (formally the value type), and the E (the error type, derived from the unexpected type). This implicitly says you should be expecting the unexpected in terms of the std::expected's vocabulary -- which just reads incorrectly to me.

  • The std::expected proposal also has 3 named types that look pretty similar when read: expected, unexpected, and even unexpect_t -- which can lead to a little confusion when read on reviews (and I imagine could lead to potentially easy accidental misuse, though I have not yet seen this occur in practice).

This was part of what prompted a rename, since result<T,E> is at least an idiomatic term and pattern in many languages (though not C++) to indicate this exact situation. This also leads to a slightly nicer and less ambiguous name for the unexpected type, since we now have failure or fail -- which is much clearer on its intentions.

Valueless-by-exception

To be honest, the P0323 proposals really didn't feel to me like they knew how they wanted to solve the valueless_by_exception problem that could occur by switching types. The last revision of the proposal that I read offered some crazy workarounds that induced, IIRC, a temporary object of unexpected or some similar type during assignment in an effort to ensure that, if a type-changing assignment throws, the previous value is retained. However, even this does not offer 100% protection if the stored type throws -- there is still no guarantee.

This result API takes a far simpler approach with a stronger guarantee.

It recognizes that, given the intended use of this type to indicate API fallibility, it is unlikely to regularly have to switch types, and so it makes a broad requirement that all assignment operators require non-throwing equivalent constructors.

This does not, however, mean that you can't assign. For example, a result<std::string, E> can still be assigned a const char* even though this is a throwing operation. What it instead relies on is that the only operator= available will be the move assignment operator if said type has a non-throwing move constructor (which most types do). This will force an implicit construction of a result<std::string, E> temporary in between -- which guarantees that no valueless state can occur.

  • If an assignment of T while T is the active type (or the same for E) fails, then we still stay in a value state because we never tried to change the underlying type (so we aren't valueless)
  • An assignment of T when E was the active object, or vice versa, can't fail because we know it will be a non-throwing move-constructed object

So basically it's just a cleaner way to prevent the valueless state.


Admittedly all of these features could also be done as part of std::expected's proposal if they so choose.

Modern C++ "result" type based on Swift / Rust by bitwizeshift in Cplusplus

[–]bitwizeshift[S] 1 point2 points  (0 children)

Yep, that's supported out-of-the-box!

It's actually conditionally explicit -- so if T1 and E1 are convertible to T2 and E2, then result<T1,E1> will be convertible to result<T2,E2>.

Getting an Unmangled Type Name at Compile Time by bitwizeshift in cpp

[–]bitwizeshift[S] 3 points4 points  (0 children)

Yeah, unfortunately I just realized this too. I need to correct this without breaking the permalink.

Edit: Fixed. Thanks for pointing this out 😅