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>

Extend INVOKE to support types convertible to target class (Revision 1)

Introduction

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

Revision history

Changes since N3719:

Motivation and Scope

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.

Existing workarounds

Defining 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.

Using lambda

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); }

Define special cast functor

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).

Comparison with alternate solutions

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:

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.

Design Decisions

Resolving ambiguity between dereference operator and conversion

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:

  1. Compilation error
  2. Preference of operator*
  3. Preference of conversion

1. Compilation error

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.

2. Preference of 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.

3. Preference of conversion

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.

Summary

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.

Preference rules for dereference operator

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.

Impact On The Standard

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.

Proposed wording

Change the paragraph 20.9.2 Requirements [func.require].

Define INVOKE(f, t1, t2, ..., tN) as follows:

  • (t1.*f)(t2, ..., tN) when f is a pointer to a member function of a class T and t1 is an object of type T or a reference to an object of type T or a reference to an object of a type derived from T;
  • ((*t1).*f)(t2, ..., tN) when f is a pointer to a member function of a class T and t1 is not one of the types described in the previous item;
  • t1.*f when N == 1 and f is a pointer to member data of a class T and t1 is an object of type T or a reference to an object of type T or a reference to an object of a type derived from T;
  • (*t1).*f when N == 1 and f is a pointer to member data of a class T and t1 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 class T:
    • (t1.*f)(t2, ..., tN) when t1 is an object of type T or a reference to an object of type T or a reference to an object of a type derived from T; otherwise
    • (to_reference<T>{}(*t1).*f)(t2, ..., tN) when *t1 is implicitly convertible to a reference to an object of type T; otherwise
    • (to_reference<T>{}(t1).*f)(t2, ..., tN) when t1 is implicitly convertible to a reference to an object of type T;
    • otherwise expression is ill-formed;
  • when f is a pointer to member data of a class T:
    • if N != 1 expression is ill-formed; otherwise
    • t1.*f when t1 is an object of type T or a reference to an object of type T or a reference to an object of a type derived from T; otherwise
    • to_reference<T>{}(*t1).*f when *t1 is implicitly convertible to a reference to an object of type T; otherwise
    • to_reference<T>{}(t1).*f when t1 is implicitly convertible to a reference to an object of type T;
    • otherwise expression is ill-formed;
  • f(t1, t2, ..., tN) in all other cases.

Implementability

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.

Acknowledgements

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.

References

  1. Alisdair Meredith, "C++ Standard Library Active Issues List (Revision R82)" (N3522, http://isocpp.org/files/papers/n3522.html)
  2. Anthony Williams, "N4033: synchronized_value<T> for associating a mutex with a value" (N4033, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4033.html)
  3. Tomasz Kamiński, Implementation of invoke function (https://github.com/tomaszkam/proposals/tree/master/invoke)