D0051R4
std::overload

Draft Proposal,

Source:
GitHub
Issue Tracking:
GitHub
Project:
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
Audience:
LWG

1. Revision History

1.1. Revision 4 - November 1st, 2018

1.2. Revision 3

1.3. Revision 2

1.4. Revision 1

This paper has been splintered into 3 or more proposals, following discussion in the Kona meeting for the original [p0051r0]:

1.5. Revision 0

2. Motivation

This paper proposes a std::overload function for the C++ Standard Library. It creates an unspecified object which uses C++ overload resolution to select one of the provided functions to call. The overloaded functions are copied and there is no way to access to the stored functions: that is another proposal.

Lambda expressions, library-defined functions objects, and other callables/INVOKEables are unable to be overloaded in the usual way, but they can be 'explicitly overloaded' using the proposed overload function. It is primarily useful for creating visitors, e.g. for variant. See below:

Shared Code
#include <string>
#include <vector>
#include <type_traits>

template<class T> 
struct always_false : std::false_type {};

void f(int value) { /* work */ }
void g(const std::string& value) { /* work */ }
auto h = [] (std::vector<int> value) { /* work */ }
Currently With Proposal
int main() {
	using my_variant = std::variant<
		int, 
		std::string, 
		std::vector<int>
	>;
	my_variant v1("bark");
	my_variant v2(2);

	auto visitor = [](auto&& value) {
		using T = std::decay_t<decltype(arg)>;
		if constexpr(std::is_same_v<T, int>) {
			f(std::forward<decltype(arg)>(value));
		}
		else if constexpr(std::is_same_v<T, std::string>) {
			g(std::forward<decltype(arg)>(value));
		}
		else if constexpr (std::is_same_v<T, std::vector<int>>) {
			h(std::forward<decltype(arg)>(value));
		}
		else {
			static_assert(always_false<T>::value, 
				"Isn’t this pretty?");
		}
	};

	// calls g
	std::visit(visitor, v1);
	// calls f
	std::visit(visitor, v2);
	// calls h
	visitor(std::vector<int>{2, 4, 6, 8});

	return 0;
}
int main() {
	using my_variant = std::variant<
		int, 
		std::string, 
		std::vector<int>
	>;
	my_variant v1("bark");
	my_variant v2(2);

	auto visitor = std::overload(f, g, h);
















	// calls g
	std::visit(visitor, v1);
	// calls f
	std::visit(visitor, v2);
	// calls h
	visitor(std::vector<int>{2, 4, 6, 8});

	return 0;
}

3. Design

std::overload is designed to work with anything that is INVOKE-able, à la std::invoke or equivalent. That means it works with functions, class types with operator(), pointer to members (member function pointers and member object pointers), and std::reference_wrapper types. This allows the full gamut of C++'s expressivity to be utilized in an overload expression.

The interface is a simple variadic function which returns an instance of an unspecified type. It is unspecified because its implementation techniques and internals are not to be relied upon by the user. As such, there is no way to retrieve a function out of the result of a std::overload call. This is intentional: if the user wants to keep a reference to the invokable, they will have to pass it in via std::ref or similar wrapper. All functions are taken by forwarding reference and behave as if stored by DECAY_COPY, with the caveat that they must behave as-presented.

The resulting call operation will be marked noexcept if the selected overload is noexcept and is_noexcept_invocable_v evaluates to true. constexpr is also propagated properly.

3.1. Perfect Argument Match vs. Forwarding?

This touches at the heart of some implementation problems. Types which introduce this problem include anything the implementation cannot derive from directly, as well as types which wrap other types and introduce forwarding argument calls:

There are 2 known ways to implement this in the face of such: one is to perfectly mimic the function call’s arguments in the case where it cannot be easily inherited into the operator() of std::overload's unspecified return type. This is usually via template specialization or similar technique that pulls out the type of every single argument (plus any object type in the case of pointer-to-members). This gives us exactly perfect overload resolution that copes with things as expected by the developer. For example: auto over = std::overload([](int) {}, [](double){}); with the first implementation strategy will always properly select the right call with over(2) and over(3.5). It comes with a cost, however: more moves and copies are performed than necessary. This is due to leaking some of the implementation details because the compiler still has to go through the "shim" layer to handle not-quite-derivable types, before hitting the real function call. Still, when used as the implementation technique for an overload set, a function that has very-close-conversions will not be ambiguous.

This was seen as a problem in previous revisions and by people who read the paper, and it was agreed that the calls should forward arguments whenever possible.

This led to the second, preferred implementation. By using a perfectly forwarding wrapper, std::overload achieves the zero-argument-passing overhead that was desirable. We performed std::enable_if_t with std::is_invocable_v<F, Args...> to SFINAE the call away when it is not appropriate. The problem became that with this strategy, the use of over(2) and over(3.5) are ambiguous if and only if one uses non-derivable types or types with forwarding calls (e.g., std::reference_wrapper<my_callable>). This presented a conundrum for the previous revisions of the paper. Ambiguity due to overload resolution based on cv-qualifiers of contained type’s operator() also happened. A regular lambda that takes a const std::string& and a mutable-marked lambda that takes a std::string_view result is an ambiguous overload set for std::overload's implementation due to the differences between const and non const.

This revision decides that it is better to make a choice and let other proposals provide more fine-grained methods of overloading. In particular, this revision chooses efficiency over overload resolution issues: by using the perfectly-forwarding, std::enable_if_t-SFINAE’d calls to handle wrapping non-derivable types, we can maintain a minimal amount of moves or copies. This is highly desirable. Additionally, the other problems can be potentially worked around by properly selecting which functions go into std::overload or writing additional lambdas, while forwarding efficiency cannot be worked around adequately by a user of std::overload. This makes the use of perfectly forwarding calls much more important for non-derivable types.

3.2. INVOKEables

The handling of all of these will be with INVOKE. The publicly available implementation does a few things extra, such as allowing for any wrapper type (not just std::reference_wrapper) to be used with pointer-to-members and some additional syntax choices, but for the sake of standardization the only requirement we are placing on implementers is that anything placed into std::overload must be INVOKEable.

3.3. Shipping

This paper has been in limbo since pre-2015, based on not getting perfectly-right implementation. There are some corners of this that do not behave exactly as expected due to forwarding call wrappers, as discussed above. However, working with std::visit is made much simpler by this: it would be better to ship this or a version of this for C++20 so that people do not continue to recreate the same thing in their codebases, but with suboptimal design choices.

It is a shame that std::visit did not ship with this feature and we do need something to fill in this gap.

4. Future

4.1. Library

When creating this paper, it was clear that other forms of overload matching might be ideal to have. For example, a std::first_overload(f1, f2, f3, ...) was discussed that would not necessarily pick the best overload not by C++ overload resolution rules, but pick the first function that was properly callable with the parameters. This means that even if one function "matches" better by C++ overload resolution rules, if a function prior to it is callable but has a worse ranking due to e.g. conversions, the function that was earlier in the list that had the conversions would be called rather than the best match. This would allow users to willingly trade between different types where conversions and other things make it ambiguous. This would also allow an explicit ranking.

This is not proposed in this paper, albeit it is strongly encouraged for others to contribute such ideas in another paper. In particular, the authors here think that a std::exact_overload/std::first_overload would be highly desirable for users who have complex variants and other corner-case ambiguities and are willing to settle for strict, perfect matching of arguments. Again, this is not proposed in this paper.

4.2. Language

Given the various problems with defining overload sets, it seems like this feature might be better fit as a language feature. Thankfully, a language solution to this problem can be developed in-parallel or even after this paper and does not need to impede the progress of this paper. A language feature addressing this problem would be to create an overloaded set out of a number of INVOKEables, and not have to suffer any of the usual problems with having to choose between different implementation techniques. This could even be an extension to [P1170R0].

5. Proposed Wording

Note: The following changes are relative to the post-Rapperswil 2018 working draft of ISO/IEC 14882, ([N4762]).

Note: The � character is used to denote a placeholder number which shall be selected by the editor.

5.1. Proposed Feature Testing Macro

The proposed feature testing macro is __cpp_lib_overload.

5.2. Intent

The intent of this wording is to produce an unspecified object that:

The implementation may not need to explicitly forward the arguments,but the specification will be written as if all arguments are perfectly forwarded from the the call on an overload-returned object and given to the proper underlying call.

5.3. Proposed Wording

Append to §16.3.1 General [support.limits.general]'s Table 35 one additional entry:

Macro name Value
__cpp_lib_overload 201811L

Insert a new entry into §19.1 [utilities.general]'s Table 38:

Subclause Header(s)
19.� Overload function objects <overload>

Insert a new sub-section §19.� Overload function objects [overload.obj] in §19 [utilities] as follows:

19.� Overload function objects [overload.obj]

19.�.1 Header <overload> synopsis [overload.obj.syn]

// 19.�, Overload
template 
  constexpr /* see below */ overload(Args&&... args) noexcept(/*see below*/)

19.�.2 overload [overload.obj.overload]

template<class... Args>;
	constexpr /* see below */ overload(Args&&... args) noexcept(/*see below*/);
Returns: An instance of an unspecified type of function object that behaves as if all the passed-in callables were overloaded (11.3 [over.match]) when calling it. The overloads shall preserve constexpr, noexcept, cv-qualifiers and reference qualifiers.

The effect of calling an instance of this type with parameters will select the best overload. Given arguments a1, a2, ... aN with types T1, T2, ..., TN for any number N >= 0, a call on the resulting object will behave as if by INVOKE(DECAY_COPY(arg), forward<T1>(a1) ..., forward<TN>(aN). If there is no such a best overload, either because there is no candidate or that there are ambiguous candidates, the invocation expression will be ill-formed.

Throws: Any exception thrown during the construction of the resulting function object. If all of the constructors satisfy is_nothrow_constructible, then the function is noexcept.
Remarks: This function as well as the overloaded operator() for each of Args on the resulting type shall be a constexpr functions. The overloaded operator() for each arg in args... on the resulting type shall be noexcept(is_nothrow_invocable_v<Arg, T1, T2, ..., TN>) or equivalent.

6. Acknowledgements

Thanks to Daniel Krügler who helped me improve the wording and pointed out to me the use case for a final Callable. Thanks to Scott Pager who suggested to add overloads for non-member and member functions (which we eventually migrated over to simply use all of INVOKE).

Thanks to Paul Fultz II and Bjørn Ali, authors of the Fit library and the FTL library, who yielded the ideas of first_overload and helped in separating the papers out from this one.

Thanks to Matt Calabrese for his useful improvement suggestions on the library usability. Thanks to Tony Van Eerd for championing the original proposal at Kona and for insightful comments.

Thanks to Stephan T. Lavavej for pointing to CWG-1581 - "When are constexpr member functions defined?". Thanks to Peter Remmers that reported issue 16.

Thanks to Tomasz Kaminski helping me to refine the implementation for final function object and to the private discussion about the possibility to have the combination of unique_overload and first_overload as a much safer solution.

Special thanks and recognition goes to Technical Center of Nokia: Lannion for supporting in part the production of this proposal.

Special thanks to Bryce Adelstein Lelbach for his notifying the new primary author of this proposal so it could be cleaned up for C++20.

References

Informative References

[N4762]
Richard Smith. Working Draft, Standard for Programming Language C++. 7 July 2018. URL: https://wg21.link/n4762
[P0051R0]
Vicente J. Botet Escriba. C++ generic overload function. 22 September 2015. URL: https://wg21.link/p0051r0
[P1170R0]
Barry Revzin, Andrew Sutton. Overload sets as function parameters. 8 October 2018. URL: https://wg21.link/p1170r0