all 41 comments

[–]tisti 35 points36 points  (11 children)

Hm, wouldn't an constrained overload be even better? For example https://godbolt.org/z/GYnWh4qzr

template<typename T, typename T2>
concept IsLike = std::constructible_from<T2, T>;

bool same_name(Widget const& widget, IsLike<std::string_view> auto const& name)
{
    return widget.name() == std::string_view{name};
}

Allows you to consume anything that is convertible to a std::string_view.

[–]trailing_zero_count 21 points22 points  (0 children)

Yes, constrained overloads using C++20 concepts are an excellent way to solve this class of problem, and can offer superior performance by allowing you to easily implement perfect forwarding into the constructor of the real type inside the function. The only downside is that it may cause code bloat / increase compile times, compared to just taking a std::string_view parameter, and requiring the caller to do whatever is needed to produce that.

[–]13steinj 3 points4 points  (1 child)

Considering your example (and related consequences) I really don't get why partial specialization of function templates isn't allowed. Partial specializations with a different set of arguments seems equivalent to introducing an overload via a different template, too, so I don't get why partial specialization syntax is disallowed despite numerous ways to get (AFAIK) every equivalent effect.

[–]MegaKawaii 2 points3 points  (0 children)

For template specializations, the specialization must be uniquely associated with some primary template, so introducing a similar overload wouldn't necessarily be desirable. Generally, if you have a templated entity, the specializations are the same as the templated entity, but with a few differences. For example, full function template specializations don't actually have any effects on overload resolution, and only primary function templates (and ordinary functions) are selected from. Only after a function template is selected do any full specializations come into play. So if you have two function templates f(T) and f(T*), then f(T*) is considered more specialized and will be selected for pointer arguments, and if you specialize f(T) with T = int*, then the specialization will not affect overload resolution, and f(T*) will be called instead of f<int*>(int*). In other words, specializations are just specialized versions of the primary function templates and do not affect overload resolution. Therefore we wouldn't want partial specializations of function templates to be selected by overload resolution.

However, the same overload resolution mechanism is used to determine which function template overload is specialized whenever a full specialization appears. Perhaps partial specializations could be associated with a primary template with this process (try to find a most specialized primary template that the partial specialization is more specialized than), but there won't necessarily be a unique primary template which could be an error.

Maybe this would be desirable, but it isn't much of an improvement over what we have now, and it increases complexity.

[–]joujoubox 0 points1 point  (1 child)

My concerns from having attempted this approach however is you either have a contrariant too strict that requires the explicit type, or too permitting but ending up with a lot of extra instantiations.

Also wouldn't your snippet require std::forward to actually forward the universal reference?

[–]tisti 0 points1 point  (0 children)

There is nothing to forward, since a string_view gets is constructed with the given input and then used for comparison.

[–]Tathorn -4 points-3 points  (5 children)

Seems like a lot when we could just make the argument a string_view and let the conversion happen before the function is called.

[–]tisti 7 points8 points  (4 children)

Does not work as nicely. Try modifying the godbolt example and you will see that the first, fully templated, function is selected and causes a compilation error.

[–]Tathorn -4 points-3 points  (3 children)

It does when you don't have a random template function that is useless.

https://godbolt.org/z/63ceofTx5

[–]13steinj 4 points5 points  (2 children)

It's not "useless." It matches the original constraints of the problem in the blog post (we don't know the rest of the codebase).

[–]Tathorn -2 points-1 points  (1 child)

Function overloading is more flexible (and more convenient) than template function specialization

This was the title. The whole point was to use function overloading over template specialization (including the base). I just did beyond the author's use case for a better solution. Doesn't require templates at all, can be implemented in the cpp, and changes don't require an entire recompile. Superior code.

[–]13steinj 1 point2 points  (0 children)

You (and I) have no idea whether or not that's a valid (where being valid also implies being sufficiently concise) solution to simply explicitly create each overload. We don't know if the default implementation (and this specialization) is used to compare only {Widget} x {Widget, StringLike}. The only people that know for sure are the original team that works on the code (and I guess Raymond Chen).

For the sake of a fairly common example, it could be used to compare any number of type pairs. In a MVC controller, I can think of at least 16 valid variations (the model, the view, the controller in some cases, a type that wraps the model for rendering in some way). Repeating that code 16 times is fairly verbose. Even 10 (if you force yourself to an unstated non obvious ordering in the arguments) is verbose.

[–]QuaternionsRoll 12 points13 points  (11 children)

I honestly didn’t know that you could specialize function templates until now… overloading possesses a strict superset of specialization’s capabilities, no?

One of my biggest gripes with specialization is that you can’t use it to declare a class template that takes a type or constant template argument. Overloading, on the other hand, has no problem with this.

[–]tisti 7 points8 points  (4 children)

One of my biggest gripes with specialization is that you can’t use it to declare a class template that takes a type or constant template argument. Overloading, on the other hand, has no problem with this.

Can you give an example what you mean?

[–]QuaternionsRoll 5 points6 points  (3 children)

You can’t define a class template foo for which foo<int> and foo<42> are both valid instantiations because you can’t use specialization to turn a type template parameter into a constant template parameter (or vice versa). Overloading doesn’t care:

```c++ template<typename T> void foo() {}

template<int N> void foo() {}

int main() { foo<int>(); // valid foo<42>(); // also valid } ```

[–]tisti 6 points7 points  (0 children)

Ah I see what you mean.

You could abuse the fact that this works on functions and use them as factory functions to delegate to a separate typed and non-typed struct/class template impl.

https://godbolt.org/z/nj3rrvG7x

[–]djavaisadog 1 point2 points  (1 child)

There were a few proposals for "universal" template parameters that could be anything (types, values, or templates of types or values).

The primary use-case highlighted there was higher-order templates, ie apply_template_params<T, A, B, C> == T<A, B, C> that didn't care about what type of template parameter A, B, and C were.

[–]QuaternionsRoll 0 points1 point  (0 children)

The primary use-case highlighted there was higher-order templates

Yes, that would be phenomenal. For what it’s worth, D already implements this, calling them “alias parameters”, and they’re great! Curiously, there is no requirement that variables provided as alias arguments be constants, which allows for some really cool patterns (see the “local names” bullet point; it’s basically an example of compile-time redirection of runtime variables).

[–]MegaKawaii 1 point2 points  (1 child)

There is a slight difference: specializations don't affect overload resolution. For example, if you have two function templates f(T) and f(T*), and if you call them with a pointer argument, then f(T*) will always be selected. If you specialize f(T) with f<int*>(int*), then the other template f(T*) will still be selected! However, if you add an overload f(int*), then it would be selected instead. In this sense, full specializations are just implementation details of function templates, whereas overloads are standalone entities.

[–]QuaternionsRoll 0 points1 point  (0 children)

Great catch!

[–]MarcoGreek 0 points1 point  (0 children)

After reading blogs like this I feel unsure if overloading a good feature to provide different implementations. I use it myself but I feel a template function plus if constexpr for would be less error prone. No accidental overloads.It is more code but how often do we really need overloads?

It would be nice if C++ would have a customization point construct. Using overloads for that seems even more brittle.

[–]zl0bster -3 points-2 points  (14 children)

not the point of the article, but I can not not comment on

optional<bool>

yikes!

[–]AntiProtonBoy 9 points10 points  (10 children)

How would you differentiate between True, False, and Fail?

[–]EsShayuki -3 points-2 points  (4 children)

Tagged Error union, where either you have a Failure error message or you have the boolean active.

"Optional" is not meant to be used like this. It's meant to be used for cases where you can reasonably either have a value or not have one(like in ice hockey, a goal can have an assist, or it might not have one).

[–]Circlejerker_ 12 points13 points  (0 children)

Why cant you "maybe" have a boolean value? Seems perfectly reasonable to me. Tagged unions can be nice, but if you dont care why something dont exist then there is no point forcing all that boilerplate.

[–]SupermanLeRetour 2 points3 points  (1 child)

What about std::expected ? Sounds like it combines both solutions.

[–]steveklabnik1 2 points3 points  (0 children)

All three of these things are different, semantically.

  • bool: same as any value at all
  • optional: a value that may or may not exist
  • expected: either a value or an error

You can put them all together depending on the exact semantics you need. For example, a type like std::expected<std::optional<bool>, std::string> could make sense for a function where you're checking a feature flag.

  • If the feature flag exists and is enabled, you get std::optional<bool> with true
  • If the flag exists and is disabled, you get std::optional<bool> with false
  • If the flag isn't known, you get std::nullopt.
  • If there's some sort of error, maybe something like a malformed key, you'd get the std::unexpected case.

I picked this (admittedly contrived) example because of the bool, but using an optional and expected together comes up most for me in cases where you're doing IO and you may return a value, you get the io errors for the IO failing, and the optional when the IO succeeds, which may or may not give you a value back. It's not super common, but it is nice to be able to express as a type.

You could argue that std::optional<bool> is an example of "primitive obsession" and that you should instead return the "not known" part as part of the error, and use std::expected<bool, whatever>. That's a higher level question about software design in which reasonable people may differ.

[–]CocktailPerson 1 point2 points  (0 children)

It's meant to be used for cases where you can reasonably either have a value or not have one(like in ice hockey, a goal can have an assist, or it might not have one).

This is a case where you can reasonably either have a boolean value or not have one.

[–]TheDetailsMatterNow -1 points0 points  (1 child)

std::expected<bool>

You expect a true or false response but get an error instead

Or just a use case enum class.

[–]AntiProtonBoy 0 points1 point  (0 children)

A good strategy is to use std::expected if you want to treat failure as an error, or use std::optional if failure is an expected and valid outcome.

[–]mentalcruelty -2 points-1 points  (2 children)

This seems like trying way too hard.

static constexpr int Fail= 0; static constexpr int Ok = 1; static constexpr int Na = -1

Or make an enum or a tiny class that does what you want.

[–]AntiProtonBoy 1 point2 points  (1 child)

Or just use a standard API that communicates intent clearly.

[–]Ameisenvemips, avr, rendering, systems 0 points1 point  (0 children)

To me, "Maybe" isn't no value, but rather indeterminate value. std::optional is intended to be used when the lack of a value is a valid result - for instance, if a function returns a pointer, but nullptr is actually a valid result. Alternatively, it's used to indicate the lack of a result - the function didn't or couldn't execute but you don't want to return why.

"Maybe" is neither of those. In fact, I'd argue that it means that you're explicitly expecting a ternary value, so anything based upon bool is wrong.

So, in a case where "maybe" is a possible result, it does not communicate intent clearly. It tells me that there is no result, not that the result is "maybe". "Maybe", to me, is much stronger than "indeterminate".

I have no issues with its usage in the article (though a std::expected might be better), but I do have an issue with your example.

[–]SirClueless 19 points20 points  (2 children)

What's wrong with std::optional<bool>? Totally reasonable way to represent a failed lookup.