Document number:   N3719
Date:   2013-08-17
Project:   Programming Language C++, Library Working Group
Reply-to:  
Tomasz Kamiński <tomaszkam at gmail dot com>

Extend INVOKE to support types convertible to target class

Introduction

This proposal is to extend the definition of INVOKE for class member pointers to cover types convertible to a target class of the pointer, like std::reference_wrapper.

Proposal also resolves LWG issue #2219

Motivation and Scope

The standard INVOKE expression models the member pointers as pair of free standing functions, that takes a reference and the pointer (including smart pointers) to target class respectively. However there is difference in semantics between the INVOKE expression for member pointers and functors - for member pointers the conversions are not taken into consideration in matching of first argument.

This behavior difference prohibit uses wrapper types (e.g., std::reference_wrapper, boost::flyweight) in combination with member pointers with the standard library functions that are modeled using INVOKE (e.g., std::bind, std::mem_fn, std::async). The aim of this proposal is to fix that usability problem via extending definition of INVOKE to allow implicit conversions in such situations.

Proposed change will also cover cases of set of convertible types that models the same logical entity (e.g., std::chrono::duration specializations). With the acceptance of this proposal, the std::bind(&std::chrono<double>::count, _1) will create functor returning amount of seconds for any specialization of std::chrono::duration.

Defining operator*

The well know 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 inelegant interface than combines wrapper and pointer semantics.

Using lambda

The other workaround is to use the lambda expression instead of library functions, but in the most cases it leads to the less readable code. Please compare 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 case of bind expressions the problem may be mitigated by 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 depends of INVOKE (e.g., std::async, std::call_once).

Design Decisions

Allowing conversion in INVOKE with member pointer may lead to ambiguity in case of entity t, for which both result of t and *t is implicitly convertible to target class of the pointer.

As 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. Existence of such in codebase class may be a result of using work around presented in the motivation section of this proposal.

There are tree possible behaviors in case of such ambiguity:

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

1. Compilation error

Raising and ambiguity error will make behaviour of INVOKE for member pointers more uniform with behaviour of free standing functions. However it will break existing code, that uses entities that are both convertible to and behaves as a pointer to target class.

2. Preference of operator*

This is the only option that allow extensions of 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. Third option is not further discussed.

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

Define viable reference types for member pointer p of type M T::* as:

  • T cv&, T cv&& for all possible cv-qualifiers cv if M is not function type,
  • T cv&, T cv&& if M is function type without ref-qualifier and with cv-qualifiers cv,
  • T cv ref if M is function type with ref-qualifier ref and cv-qualifiers cv.

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

  • when f is a pointer to a member function of a class T and TR is viable reference type for f:
    • (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;
    • (TR{*t1}.*f)(t2, ..., tN) when t1 does not fulfill criteria of any of previous point and *t1 is implicitly convertible to TR;
    • (TR{t1}.*f)(t2, ..., tN) when t1 does not fulfill criteria of any of previous point and t1 is implicitly convertible to TR;
    • otherwise expression is ill-formed;
  • when f is a pointer to member data of a class T and N == 1 and TR is viable reference type for f:
    • 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;
    • TR{*t1}.*f when t1 does not fulfill criteria of any of previous point and *t1 is implicitly convertible to TR;
    • TR{t1}.*f when t1 does not fulfill criteria of any of previous point and t1 is implicitly convertible to TR;
    • otherwise expression is ill-formed;
  • f(t1, t2, ..., tN) in all other cases.

Alternate proposal

Change the paragraph 20.10.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.

Define viable reference types for member pointer p of type M T::* as:

  • T cv&, T cv&& for all possible cv-qualifiers cv if M is not function type,
  • T cv&, T cv&& if M is function type without ref-qualifier and with cv-qualifiers cv,
  • T cv ref if M is function type with ref-qualifier ref and cv-qualifiers cv.

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

  • when f is a pointer to a member function of a class T and TR is viable reference type for f:
    • (TR{t1}.*f)(t2, ..., tN) when t1 is implicitly convertible to TR;
    • (TR{*t1}.*f)(t2, ..., tN) when *t1 is implicitly convertible to TR;
    • the expression is ill-formed when neither or both of above points applies;
  • when f is a pointer to member data of a class T and N == 1 and TR is viable reference type for f:
    • TR{t1}.*f when t1 is implicitly convertible to TR;
    • TR{*t1}.*f when *t1 is implicitly convertible to TR;
    • the expression is ill-formed when neither or both of above points applies;
  • 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 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. Tomasz Kamiński, Implementation of invoke function (https://github.com/tomaszkam/proposals/tree/master/invoke)