Document number: | N4170 | |
---|---|---|
Date: | 2014-08-12 | |
Project: | Programming Language C++, Library Evolution Working Group | |
Reply-to: | Tomasz Kamiński <tomaszkam at gmail dot com> |
This proposal extends the definition of INVOKE
for class member pointers to cover types convertible to the target class of the pointer, like std::reference_wrapper
.
Proposal also resolves LWG issue #2219
Changes since N3719:
The definition of INVOKE
in the Standard handles pointers to members
by defining two free standing functions: one that takes a reference and the other that takes
a pointer (including smart pointers) to target class.
However, there is a difference in semantics between the INVOKE
expression
for pointers to members and functors: for pointers to members the conversions are not taken into
consideration when matching the first argument.
This difference in behaviour prohibits the uses of wrapper types (e.g., std::reference_wrapper
,
boost::flyweight
) in combination with pointers to members inside the Standard Library functions
that are defined in terms of INVOKE
(e.g., std::bind
, std::mem_fn
,
std::async
). The aim of this proposal is to fix that usability problem by extending the definition of
INVOKE
to allow implicit conversions in such situations.
The proposed change will also cover the cases like std::chrono::duration
specializations,
where a family of types convertible to one common 'base' model the same logical entity.
With the acceptance of this proposal, expression
std::bind(&std::chrono<double>::count, _1)
will create a functor returning the number of
seconds for any specialization of std::chrono::duration.
In addition, this proposal allows the conversion to the target type to be applied
on the result of the dereference operator. This handles types similar to
synchronized_value
proposed in N4033.
operator*
A well known workaround for this problem, is to define the operator*
that will return the same result as the conversion operator. Firstly,
this solution is only applicable in situations when the definition of
the class can be changed, so it is not feasible for third-party library
classes. Secondly, it leads to an inelegant interface that combines wrapper and
pointer semantics.
The other workaround is to use the lambda expression instead of library functions, but in most cases it leads to a less readable code. Compare the following code snippets:
std::bind(&foo, _1, expr, ref(a)); [e = expr, &a] (auto&& arg) -> decltype(auto) { return foo(std::forward<decltype(arg)>(arg), e, a); }
In the case of bind
expressions the problem may be mitigated by the introduction of additional cast functor
that preforms required casting.
std::bind(&Class::method, _1)(std::ref(clazz)); std::bind(&Class::method, cast<Class&>(_1))(std::ref(clazz));
However, this solution depends on std::is_bind_expression
trait and cannot be applied
to other library components that are defined in terms of INVOKE
(e.g., std::async
, std::call_once
).
LWG issue #2219 proposes that INVOKE
be specialized for class template reference_wrapper
. Indeed, if we take a look at the examples above, we may
find that the only one, that is commonly used is reference_wrapper
. This requires less changes to the
standardese.
The problem with this solution is that it is not taking into account the need to support user-defined types, both existing ones and ones developed in future. In contrast, the solution presented in this proposal is consistent with the design of the standard library which aims to provide equal support both for user-defined types and standard components, even at the cost of the increased complexity of the definition. Most notable examples include:
begin
/end
operations
(both as member or free function found by argument depended lookup);condition_variable_any
is provided in addition to condition_variable
,
to provide to support for any lock type;INVOKE
expression is supporting any deference type that returns reference to type compatible
with member class.Furthermore, it has been suggested that if a user-defined type needs to work with INVOKE
,
it can achieve the goal by defining operator*
. If this were to be a valid advice, we would expect it
to also apply to reference_wrapper
. However, in the case of reference_wrapper
a special
dedicated solution is being proposed, which indicates that the original advice is insufficient; probably also for
user-defined types.
In conclusion, the author perceives the addition of a single exception to the standard library in order to support a single standard class, a non-feasible solution, both from the language learning perspective and the usability of the language, especially in the context of designing library that should interoperate with standard ones.
Allowing the conversion in INVOKE
for pointers to member may lead to an ambiguity in the case of entity t
for which both the result of t
and *t
is implicitly convertible to target class of the pointer.
For example, for the following class:
struct Clazz { int foo; } struct Mixed { Clazz& operator*(); operator Clazz&(); }; Mixed m;
The expression INVOKE(&Clazz::foo, m)
may be interpreted as static_cast<Clazz>(m).*foo
or static_cast<Clazz>(*m).*foo
. The existence of such class in codebase might be the result of using a
work around presented in the motivation section of this proposal.
There are tree possible resolutions of such ambiguity:
operator*
Rising an error will make the behaviour of INVOKE
for member pointers more uniform with the behaviour of free standing
functions. However, it will break existing code that uses such entities.
operator*
This is the only option that extends INVOKE
definition without breaking or introducing silent behaviour changes in
the existing code. The minor drawback is that it leads to more
complicated definition of INVOKE
.
Preference of the conversion leads to the silent behaviour change of the existing C++11 standard compliant code, so this option should not be considered as a feasible solution.
This proposal recommends implementing the second option and provides the wording in the Proposed wording section. The wording for the first option may be found in the Alternate proposal section of N3719. Third option is not further discussed.
According to wording presented in the first version of this proposal dereference operator was preferred over conversion if it's result type was compatible with cv-qualification and ref-qualification of member function pointer. For example, given the following definitions:
struct Clazz { void foo(); }; struct Wrapper { A const& operator*(); operator A&(); };
expression std::mem_fn(&A::foo)(Wrapper())
will use conversion operator
because member function foo
cannot be invoked on a const object.
As a consequence of the above behaviour, the modification of cv-qualification of the function may lead
to silent code changes. Let's imagine the situation in which member function foo
becomes const-qualified; such modification will silently change meaning of expression std::mem_fn(&A::foo)(Wrapper())
,
that will invoke dereference operator instead of conversion.
The author considers such change unacceptable because -- unlike in the case when we change one function overload for another --
we cannot reliably expect that dereference operator and conversion wiould have similar behaviour.
In order to avoid the described problem, the current revision of the paper includes new rules for selecting the dereference operator: the deference operator is selected if it returns a type that is implicitly convertible to a reference to target class of member pointer, however cv-qualified.
The proposed resolution keeps the above example ill-formed, preserving behaviour defined in the current standard.
In addition, it greatly simplifies usage of components defined in terms of INVOKE
,
by making their behaviour for pointers to members independent of the actual nature of the member: data or function.
Please also note that the wording change is only impacting cases of ambiguous wrappers that define both dereference operator and conversion. Furthermore, the problem would not be present, if compilation error approach was selected as resolution in such situations.
This proposal has no dependencies beyond a C++11 compiler and
Standard Library implementation. (It depends on perfect forwarding,
varidatic templates, decltype
and trailing return types.)
Nothing depends on this proposal.
Change the paragraph 20.9.2 Requirements [func.require].
Define
INVOKE(f, t1, t2, ..., tN)
as follows:
(t1.*f)(t2, ..., tN)
whenf
is a pointer to a member function of a classT
andt1
is an object of typeT
or a reference to an object of typeT
or a reference to an object of a type derived fromT
;((*t1).*f)(t2, ..., tN)
whenf
is a pointer to a member function of a classT
andt1
is not one of the types described in the previous item;t1.*f
whenN == 1
andf
is a pointer to member data of a classT
andt1
is an object of typeT
or a reference to an object of typeT
or a reference to an object of a type derived fromT
;(*t1).*f
whenN == 1
andf
is a pointer to member data of a classT
andt1
is not one of the types described in the previous item;f(t1, t2, ..., tN)
in all other cases.
Given the exposition only functor:
template<typename T> struct to_reference { T& operator()(T& t) const { return t; } T const& operator()(T const& t) const { return t; } T volatile& operator()(T volatile& t) const { return t; } T const volatile& operator()(T const volatile& t) const { return t; } T&& operator()(T&& t) const { return std::move(t); } T const&& operator()(T const&& t) const { return std::move(t); } T volatile&& operator()(T volatile&& t) const { return std::move(t); } T const volatile&& operator()(T const volatile&& t) const { return std::move(t); } };Define
INVOKE(f, t1, t2, ..., tN)
as follows:
- when
f
is a pointer to a member function of a classT
:
(t1.*f)(t2, ..., tN)
whent1
is an object of typeT
or a reference to an object of typeT
or a reference to an object of a type derived fromT
; otherwise(to_reference<T>{}(*t1).*f)(t2, ..., tN)
when*t1
is implicitly convertible to a reference to an object of typeT
; otherwise(to_reference<T>{}(t1).*f)(t2, ..., tN)
whent1
is implicitly convertible to a reference to an object of typeT
;- otherwise expression is ill-formed;
- when
f
is a pointer to member data of a classT
:
- if
N != 1
expression is ill-formed; otherwiset1.*f
whent1
is an object of typeT
or a reference to an object of typeT
or a reference to an object of a type derived fromT
; otherwiseto_reference<T>{}(*t1).*f
when*t1
is implicitly convertible to a reference to an object of typeT
; otherwiseto_reference<T>{}(t1).*f
whent1
is implicitly convertible to a reference to an object of typeT
;- otherwise expression is ill-formed;
f(t1, t2, ..., tN)
in all other cases.
Proposed change can be implemented as pure library extension in C++11. Implementation of invoke
function that conforms proposed wording can be found https://github.com/tomaszkam/proposals/tree/master/invoke.
Tomasz Miąsko, Andrzej Krzemieński and Mikhail Semenov offered many useful suggestions and corrections to the proposal.
Ville Voutilainen, Gabriel Dos Reis and other people in discussion group ISO C++ Standard - Future Proposals provided numerous insightful suggestions.
synchronized_value<T>
for associating a mutex with a value" (N4033, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4033.html)