Qualified std::function signatures

Similar documents
A polymorphic wrapper for all Callable objects (rev. 3)

New wording for C++0x Lambdas

Rvalue References as Funny Lvalues

Variant: a type-safe union without undefined behavior (v2).

A Taxonomy of Expression Value Categories

P0984R0 - All (*)()-Pointers Replaced by Ideal Lambdas Functions Objects Obsoleted by Lambdas

A polymorphic value type for C++

Operator Dot Wording

C The new standard

A polymorphic value-type for C++

Implicit Evaluation of auto Variables and Arguments

A polymorphic value-type for C++

Axivion Bauhaus Suite Technical Factsheet AUTOSAR

Variadic Templates for the C++0x Standard Library

std::optional from Scratch

Introduction. Contents. Homogeneous interface for variant<ts...>, any and optional<t>

Proposed Wording for Concurrent Data Structures: Hazard Pointer and Read Copy Update (RCU)

Revised Latches and Barriers for C++20

P0591r4 Utility functions to implement uses-allocator construction

A jthread automatically signals an interrupt at the end of its lifetime to the started thread (if still joinable) and joins:

Apply the following edits to N4741, the working draft of the Standard. The feature test macros cpp_lib_latch and cpp_lib_barrier should be created.

C++11/14 Rocks. Clang Edition. Alex Korban

register lock_guard(mtx_); string_view s = register to_string(42); We propose register-expression to grant the temporary objects scope lifetimes.

A Cooperatively Interruptible Joining Thread, Rev 4 New in R4

Lambda Expressions and Closures: Wording for Monomorphic Lambdas (Revision 3)

Tokens, Expressions and Control Structures

Overload Resolution. Ansel Sermersheim & Barbara Geller Amsterdam C++ Group March 2019

Introduction to C++ Systems Programming

Splicing Maps and Sets (Revision 1)

Variant: a type-safe union that is rarely invalid (v5).

Overload Resolution. Ansel Sermersheim & Barbara Geller ACCU / C++ June 2018

Accessors and views with lifetime extension

Towards A (Lazy) Forwarding Mechanism for C++

p0408r4 - Efficient Access to basic_stringbuf s Buffer Including wording from p0407 Allocator-aware basic_stringbuf

void fun() C::C() // ctor try try try : member( ) catch (const E& e) { catch (const E& e) { catch (const E& e) {

Generalized pointer casts

Variant: a typesafe union. ISO/IEC JTC1 SC22 WG21 N4218

Appendix. Grammar. A.1 Introduction. A.2 Keywords. There is no worse danger for a teacher than to teach words instead of things.

Structured bindings with polymorphic lambas

Interview Questions of C++

C++11 and Compiler Update

C++11: 10 Features You Should be Using. Gordon R&D Runtime Engineer Codeplay Software Ltd.

Variant: a type-safe union (v3).

Constraining unique_ptr

Database Systems on Modern CPU Architectures

These new operators are intended to remove some of the holes in the C type system introduced by the old C-style casts.

memory_resource_ptr: A Limited Smart Pointer for memory_resource Correctness

Modern and Lucid C++ Advanced for Professional Programmers. Part 3 Move Semantics. Department I - C Plus Plus Advanced

A Standard flat_map. 1 Revisions. 2 Introduction. 3 Motivation and Scope. 1.1 Changes from R Changes from R0

call/cc (call-with-current-continuation): A low-level API for stackful context switching

Bind Returned/Initialized Objects to the Lifetime of Parameters, Rev0

Lambda Expressions and Closures: Wording for Monomorphic Lambdas

Operator Dot Wording

Object-Oriented Principles and Practice / C++

Proposed Wording for Concurrent Data Structures: Hazard Pointer and Read Copy Update (RCU)

Intro to OOP Visibility/protection levels and constructors Friend, convert constructor, destructor Operator overloading a<=b a.

Modules: ADL & Internal Linkage

Working Draft, Technical Specification for C++ Extensions for Parallelism

async and ~future (Revision 3)

Fixing Atomic Initialization, Rev1

Proposal to Simplify pair (rev 4)

Implicit Evaluation of auto Variables and Arguments

Lambda Expressions and Closures: Wording for Monomorphic Lambdas (Revision 2)

EXP54-CPP. Do not access an object outside of its lifetime

Interrupt Tokens and a Joining Thread, Rev 7 New in R7

American National Standards Institute Reply to: Josee Lajoie

Object-Oriented Principles and Practice / C++

A Proposal to Add a Const-Propagating Wrapper to the Standard Library

Deducing the type of variable from its initializer expression (revision 3)

Generalized Constant Expressions

Generalized lifetime extension!

More C++ : Vectors, Classes, Inheritance, Templates. with content from cplusplus.com, codeguru.com

Short Notes of CS201

Object-Oriented Programming

Modern C++ for Computer Vision and Image Processing. Igor Bogoslavskyi

Fast Introduction to Object Oriented Programming and C++

Template Issue Resolutions from the Stockholm Meeting

Let return Be Direct and explicit

Introduction to Move Semantics in C++ C and C

I m sure you have been annoyed at least once by having to type out types like this:

CS201 - Introduction to Programming Glossary By

A Standard flat_set. This paper outlines what a (mostly) API-compatible, non-node-based set might look like.

Deducing the type of variable from its initializer expression (revision 4)

Efficient concurrent waiting for C++20

Supporting async use-cases for interrupt_token

Efficient waiting for concurrent programs

Type Inference auto for Note: Note:

<Insert Picture Here> GCC and C++, again

OBJECT ORIENTED PROGRAMMING USING C++ CSCI Object Oriented Analysis and Design By Manali Torpe

Working Draft, Extensions to C++ for Modules

CPSC 427: Object-Oriented Programming

p0448r1 - A strstream replacement using span<chart> as buffer

A Unit Type for C++ Introduction. Related proposals. Proposed changes

Type Erasure. Nevin :-) Liber Friday, October 29, Chicago C/C++ Users Group Nevin J. Liber All Rights Reserved.

Hazard Pointers. Safe Resource Reclamation for Optimistic Concurrency

ECE 449 OOP and Computer Simulation Lecture 12 Resource Management II

Basic Types, Variables, Literals, Constants

Proposed Resolution to TR1 Issues 3.12, 3.14, and 3.15

Chapter 15 - C++ As A "Better C"

Class Types in Non-Type Template Parameters

Transcription:

Document number: P0045R1 Date: 2017 02 06 To: SC22/WG21 LEWG Reply to: David Krauss (david_work at me dot com) Qualified std::function signatures std::function implements type-erasure of the behavior of the call operator on an object, but it cannot emulate every function signature exactly, resulting in a couple rough edges. The qualified signatures added by this proposal improve const safety of the call operator (e.g. function<int() const>) and enable one-shot function types (e.g. function<int() &&>). This feature also meshes with proposals for non-copyable (P0288R1) and overloaded call wrappers. A quality prototype implementation is provided. 1 1. Background Upon reviewing N4159, LEWG resolved (issue 34) to pursue several extensions to std::function, including accepting target objects that can only be called as rvalue expressions, e.g. std::move( target ) ( arg1, arg2 ). N4159 also raises the issue of const safety in function. For example, this works: struct delay_buffer { int saved = 42; int operator () ( int i ) { return std::exchange( saved, i ); } }; // Small object optimization no heap allocation. const std::function< int( int ) > f = delay_buffer{}; assert ( f( 1 ) == 42 ); assert ( f( 5 ) == 1 ); // A const object has changed state. This example is troublesome because the object f is truly const (it s not only getting accessed that way), and the target object being allocated within it should also be truly const. 2. Qualified signatures The crux of this proposal is to use the template type parameter of std::function as the type of its call operator function, including &, &&, const, and noexcept qualifiers. When a target object is adopted, it is checked for compatibility ([func.wrap.func] 20.14.12.2/2 2), with the additional constraint that any cv-qualifier-seq or ref-qualifier in the signature is applied to the function object. If no ref-qualifier is present, then & is used as in the current Lvalue-Callable constraint. 1 2 https://github.com/potswa/cxx_function; no relation to CxxFunctionBenchmark by Tongari J. Citations in [cross.ref] 1.2/3 format refer to the working draft N4618. 1

The critical expression used for constructor SFINAE becomes (with a default ref-qualifier of &): INVOKE(declval<F cv-qualifier-seqopt ref-qualifier >(), declval<argtypes>()..., R) This proposal changes classic specializations such as std::function<void()> so that the call operator loses const qualification. This renders types such as const function<void()> & uncallable. This breakage is remedied by an additional, [[deprecated]] call operator with const qualification and a const_cast inside. The fix applies only to wrappers with unqualified signatures, so e.g. const function<void()&> & is not callable. std::function< void() > f = []{}; f = []() mutable {}; f(); auto const & fcr = f; fcr(); // Deprecation warning: Legacy, loss of const safety. std::function< void() const > fc = []{}; auto const & fccr = fc; fccr(); fc = []() mutable {}; // Error: Target does not support the const-qualified signature. 2.1. const compatibility The const-unsafe behavior happens when a user passes a std::function by const& reference instead of by value or forwarding. This must be supported for now, but it should be discouraged by deprecation. Whenever the const-correctness issue is diagnosed, users should be informed of three solutions. Adapted from the documentation of the prototype library: 1. Pass the function by value or forwarding so it is not observed to be const. This has always been the canonical usage of std::function (and all Callable objects). This fix can be applied per call site, usually without affecting any external interface. 2. Add a const qualifier to the signature. This explicitly solves the problem through greater exposition and tighter constraints. It requires that the target object be callable as const. This is usually a good idea in any case, for functions that are not stateful. Ideally, function< void() const > should represent a function that does the same thing on each call. Having function< void() > const means that a call may change some state, but the wrapper doesn t have permission to do so. (This is the crux of the issue.) If the target needs to change, but only in a way that doesn't affect its observable behavior, consider using mutable instead. Note that lambdas allow mutable, but the keyword is somewhat abused: the members of the lambda are not mutable. It only makes the call operator non-const. A class must be explicitly defined with a mutable member. 3. Consistently remove const from the reference which gets called. Non-const references are the best way to share mutable access to values. More const is not necessarily better. Again, this reflects greater exposition and tighter constraints. 2

If a user must use a const_cast, do so within the target call handler, not on the function wrapper which gets called. This allows the hack to be applied once and for all, and affecting the narrowest set of objects. The validity of const_cast depends on whether or not the affected object is truly const, and even when the wrapper is, the target may not be. 2.2. Double wrapping When a value is converted from one polymorphic wrapper type to another, the user may intend to transfer the original target object, or simply to preserve its behavior. Instead, the given wrapper value becomes the target of the new wrapper. The small-object optimization cannot apply in this situation, so the heap must be used. Unintended conversions of this sort potentially cause performance and memory usage issues. void printint( int i ) { std::cout << i << '\n'; } std::function< void( int ) > fi = &printint; std::function< void( long ) > fl = fi;, but double wrapping. assert ( fl.target_type() == typeid( &printint ) ); // Fails. Qualified signatures open a new avenue to the problem. The user might initialize a one-shot wrapper from a wrapper with an unqualified signature, or add const qualification where there was none. Interoperation with classic wrapper types is likely to be common. std::function<void(int) &&> one_shot = fi; std::function<void(int) const> const_safe = fi;, no overhead. // Deprecated, but no overhead. Fortunately, double wrapping is unnecessary for the proposed types. The type-erasure object containing the target can be made interoperable with wrapper objects of any function qualifiers, simply by ignoring them. Safety is guaranteed by the converting constructor or assignment operator. fi = one_shot; // Error: cannot invoke one_shot as lvalue. Elimination of double wrapping also nicely reduces the number internal target specializations, and helps ensure some commonality between wrapper templates. 2.3. volatile Objects of volatile-qualified class type exist, albeit rarely. For example, there are volatilequalified member overloads in std::atomic. This proposal permits such signatures for function, serving as an annotation that the target may modify a device register. Calling through a volatile access path invokes no special semantics. There is no volatilequalified assignment operator so a volatile wrapper can never be reassigned. 3

2.4. noexcept Noexcept-qualified call signatures indicate that invocation must not throw. Since defaultconstructed wrappers always throw, a wrapper with such a call signature is not defaultconstructible. std::function<void() noexcept> fn1; // Error: not default-constructible. std::function<void() noexcept> fn2 = []{}; // Error: target is not noexcept. std::function<void() noexcept> fn3 = []() noexcept {};. 3. Standardese These changes are relative to N4618. Currently in [func.def] 20.14.1/2, call signature constructs a restricted form of function type. Loosen the restrictions, and let it properly be the type. 2 A call signature is the name of a return type followed by a parenthesized comma-separated list of zero or more argument types. a function type [dcl.fct] which describes the behavior of a function object type [function.objects], by serving as the type of a function call operator [over.call]. A function type whose parameter-type-list ends with an ellipsis may not be a call signature. The call-ref-qualifier of a call signature is its ref-qualifier, if any, or otherwise &. Remove the specified class template partial specialization from the synopsis of [func.wrap.func] 20.14.12.2. Specify the behavior of the primary template instead. template<class Sig> class function ; // undefined template<class R, class... ArgTypes> class function<r(argtypes...)> { public: Subsequently in the same synopsis, modify the declaration of the call operator. Add the deprecated signature as well. // 20.14.12.2.4, function invocation R operator()(argtypes...) const qualifiers; // R, ArgTypes..., and qualifiers are the return type, the parameter-type-list, // and the sequence cv-qualifier-seqopt ref-qualifieropt noexcept-specifieropt // of the function type Sig, respectively. [[deprecated]] R operator()(argtypes...) const; // only if qualifiers is an empty sequence Likewise change the template parameters of the free functions. // 20.14.12.2.6, Null pointer comparisons bool operator==(const function<r(argtypes...) Sig>&, nullptr_t) 4

bool operator==(nullptr_t, const function<r(argtypes...) Sig>&) bool operator!=(const function<r(argtypes...) Sig>&, nullptr_t) bool operator!=(nullptr_t, const function<r(argtypes...) Sig>&) // 20.14.12.2.7, specialized algorithms void swap(function<r(argtypes...) Sig>&, function<r(argtypes...) Sig>&) Change the target object viability test in 2 and the wrapper capability description in 3 to reflect the change to call signature. 2 A callable type F is Lvalue-Callable for argument types ArgTypes and return type R a call signature R(ArgTypes...) cv-qualifier-seqopt ref-qualifieropt noexcept-specifieropt if the expression noexcept(invoke(declval<f& cv-qualifier-seqopt call-ref-qualifier>(), declval<argtypes>()..., R)), considered as an unevaluated operand ([expr]), is well formed ([func.require]), where call-ref-qualifier is the call-ref-qualifier of the signature ([func.def]), and if the noexcept-specifier is present, the expression evaluates to true. 3 The function class template A specialization function<sig> is a call wrapper ([func.def]) whose call signature is R(ArgTypes...) Sig. Modify [func.wrap.func.con] 20.14.12.2.1 to prohibit constructing null-valued, noexceptqualified wrappers. function() 1 Postconditions: [ ]? Remarks: This constructor shall be defined as deleted if Sig is a noexcept function type. function(nullptr_t) 2 Postconditions: [ ]? Remarks: This constructor shall be defined as deleted if Sig is a noexcept function type. Update the converting constructor specification and modify it to prevent double wrapping. template<class F> function(f f); 7 Requires: [ ] 8 Remarks: This constructor template shall not participate in overload resolution unless f is Lvalue-Callable ([func.require]) for argument types ArgTypes... and return type R the call signature Sig. 5

9 Postconditions: [ ]? Otherwise, if F is a specialization of the function class template, and the return and parameter types of its call signature are respectively identical to those of Sig, then the target of *this is the target of f or a move-constructed object of the same type. 10 Otherwise, Likewise update the converting assignment operator specification. 21 Remarks: This assignment operator shall not participate in overload resolution unless decay_t<f> is Lvalue-Callable ([func.require]) for argument types ArgTypes... and return type R the call signature Sig. Modify [func.wrap.func.inv] 20.14.12.2.4 to reflect the new feature. R operator()(argtypes...) const qualifiers;? R, ArgTypes..., and qualifiers are the return type, the parameter-type-list, and the sequence cv-qualifier-seqopt ref-qualifieropt noexcept-specifieropt of the call signature Sig, respectively. 1 Returns: INVOKE(std::forward<F cv-qualifier-seqopt call-ref-qualifier >(f), std::forward<argtypes>(args)..., R) (20.9.2), where f is names the target object (20.9.1) of *this, F is its type, and cv-qualifier-seqopt and call-ref-qualifier come from the call signature ([func.def]). 2 Throws: bad_function_call if!*this; otherwise, any exception thrown by the wrapped callable object. [[deprecated]] R operator()(argtypes...) const;? Remarks: This member shall participate in overload resolution only if qualifiers is an empty sequence. Its use is deprecated, including use in an unevaluated INVOKE expression while determining that the class is Callable for a different call signature.? [Note: This overload allows non-const access to the target object of a const-qualified function object, which may be unsafe ([dcl.type.cv]). Several strategies may help to improve usage of const, to avoid calling this overload: 1. Pass the function object by value, by forwarding reference, or by non-const reference, to avoid forming a reference to const-qualified type. Then, each invocation may legitimately modify the target object. 2. Add a const qualifier to the call signature Sig, to forbid modification of the target object. It may be necessary, in turn to add const qualifiers to target class call operators. Ideally, qualification like function<void() const> should always be used when the same effect is expected over successive invocations, and function<void()> should only be used when the object s value or effect may vary. 3. As a last resort, add const_cast at the site of the deprecated call. Ensure that the referent object is not const ([dcl.type.cv]). end note] 6

? Returns: INVOKE(f, std::forward<argtypes>(args)..., R) ([func.require]), where f names the target object ([func.def]) of *this.? Throws: bad_function_call if!*this; otherwise, any exception thrown by the wrapped callable object. Change the free function template signatures in [func.wrap.func.nullptr] 20.14.12.2.6 to avoid decomposing the call signature. bool operator==(const function<r(argtypes...) Sig>& f, nullptr_t) bool operator==(nullptr_t, const function<r(argtypes...) Sig>& f) 1 Returns:!f. bool operator!=(const function<r(argtypes...) Sig>& f, nullptr_t) bool operator!=(nullptr_t, const function<r(argtypes...) Sig>& f) 1 Returns: (bool)f. Likewise in [func.wrap.func.alg] 20.14.12.2.7. template<class R, class... ArgTypes typename Sig> void swap(function<r(argtypes...) Sig>& f1, function<r(argtypes...) Sig>& f2) 1 Effects: As if by: f1.swap(f2); Finally, add a feature test macro, cpp_lib_qualified_call_signature. 4. Future directions Several problems remain to be potentially addressed in future proposals. 4.1. Unqualified wrappers ignore rvalue target behavior If and when a reflective facility allows identification of the overload chosen for a given call expression, call wrappers without a ref-qualified call signature may require that the target object behaves the same for & and && qualified call signatures. 7

4.2. Unsafe but useful conversions between specializations Conversion of function<void()> to function<void() noexcept> or function<void() const> is disabled by SFINAE. The workaround is to manually doublewrap, e.g. through a lambda expression. Such conversions should be defined but explicit, following the usual pattern that the inverse of an implicit conversion is allowed through explicit syntax. Double-wrapping would then be avoided. 5. Conclusion Qualified call wrapper signatures help to support fundamental principles: const safety since early C++ and rvalue propagation since C++11. They are overdue. 5.1. Kudos Kudos to Geoff Romer for pursuing and advocating the overall direction. 5.2. Revision history P0045R0 Initial revision, as Overloaded and qualified std::function. P0045R1 Remove overloading proposal. Require the [[deprecated]] attribute. Remove discussion of implementation issues and concurrency. Add noexcept signatures. Add normative wording. Update future directions. 8