Structured bindings with polymorphic lambas

Similar documents
C The new standard

Expansion statements. Version history. Introduction. Basic usage

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

Tokens, Expressions and Control Structures

Object Oriented Design

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

Evolution of Programming Languages

Botet C++ generic match function P0050

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

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

Interview Questions of C++

Rvalue References as Funny Lvalues

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

New wording for C++0x Lambdas

Allowing Class Template Specializations in Associated Namespaces (revision 1)

C++11 and Compiler Update

Extending Classes (contd.) (Chapter 15) Questions:

I BCS-031 BACHELOR OF COMPUTER APPLICATIONS (BCA) (Revised) Term-End Examination. June, 2015 BCS-031 : PROGRAMMING IN C ++

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

Variables. Data Types.

Introduction to C++ Introduction to C++ Dr Alex Martin 2013 Slide 1

Lambdas in unevaluated contexts

American National Standards Institute Reply to: Josee Lajoie

Modules: ADL & Internal Linkage

10. Functions (Part 2)

Absolute C++ Walter Savitch

Short Notes of CS201

OOPS Viva Questions. Object is termed as an instance of a class, and it has its own state, behavior and identity.

CS201 - Introduction to Programming Glossary By

CS93SI Handout 04 Spring 2006 Apr Review Answers

AN OVERVIEW OF C++ 1

Deducing the type of variable from its initializer expression Programming Language C++ Document no: N1721=

Where do we go from here?

Chapter 17: Nested Classes

Implicit Evaluation of auto Variables and Arguments

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

W3101: Programming Languages C++ Ramana Isukapalli

Auxiliary class interfaces

Increases Program Structure which results in greater reliability. Polymorphism

CS 251 INTERMEDIATE SOFTWARE DESIGN SPRING C ++ Basics Review part 2 Auto pointer, templates, STL algorithms

Object-Oriented Programming (OOP) Fundamental Principles of OOP

Proposal to Simplify pair (rev 4)

A Taxonomy of Expression Value Categories

Copyie Elesion from the C++11 mass. 9 Sep 2016 Pete Williamson

CS 247: Software Engineering Principles. C++ lambdas, bind, mem_fn. U Waterloo CS247 (Spring 2017)

Lambdas in unevaluated contexts

CS

CS 162, Lecture 25: Exam II Review. 30 May 2018

CS3157: Advanced Programming. Outline

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

Introduction to C++ Systems Programming

CPSC 427: Object-Oriented Programming

1/29/2011 AUTO POINTER (AUTO_PTR) INTERMEDIATE SOFTWARE DESIGN SPRING delete ptr might not happen memory leak!

POLYMORPHISM 2 PART. Shared Interface. Discussions. Abstract Base Classes. Abstract Base Classes and Pure Virtual Methods EXAMPLE

POLYMORPHISM 2 PART Abstract Classes Static and Dynamic Casting Common Programming Errors

Fast Introduction to Object Oriented Programming and C++

C++ Important Questions with Answers

Proposed Resolution to TR1 Issues 3.12, 3.14, and 3.15

explicit class and default definitions revision of SC22/WG21/N1582 =

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

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

Introduce C# as Object Oriented programming language. Explain, tokens,

An Introduction to Template Metaprogramming

An introduction to C++ template programming

Recap: Can Specialize STL Algorithms. Recap: Can Specialize STL Algorithms. C++11 Facilities to Simplify Supporting Code

The pre-processor (cpp for C-Pre-Processor). Treats all # s. 2 The compiler itself (cc1) this one reads text without any #include s

04-19 Discussion Notes

Lesson 13 - Vectors Dynamic Data Storage

CE221 Programming in C++ Part 1 Introduction

COEN244: Class & function templates

Outline. 1 About the course

Value categories. PRvalues. Lvalues

Exception Namespaces C Interoperability Templates. More C++ David Chisnall. March 17, 2011

Technical Specification: Concepts

Problem Solving with C++

G52CPP C++ Programming Lecture 16

Botet C++ generic overload functions P0051

Abstract This paper proposes the ability to declare multiple variables initialized from a tuple or struct, along the lines of:

Cpt S 122 Data Structures. Course Review Midterm Exam # 2

Abstract This paper proposes the ability to declare multiple variables initialized from a tuple or struct, along the lines of:

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

Weiss Chapter 1 terminology (parenthesized numbers are page numbers)

Advanced C++ Programming Workshop (With C++11, C++14, C++17) & Design Patterns

Qualified std::function signatures

Part X. Advanced C ++

Exceptions, Case Study-Exception handling in C++.

COMP6771 Advanced C++ Programming

EL2310 Scientific Programming

September 10,

2 is type double 3 auto i = 1 + 2; // evaluates to an 4 integer, so it is int. 1 auto d = 5.0; // 5.0 is a double literal, so it

COMP322 - Introduction to C++

Operator Dot Wording

Laboratorio di Tecnologie dell'informazione

CS11 Introduction to C++ Fall Lecture 6

STRUCTURING OF PROGRAM

C++ Programming Lecture 6 Software Engineering Group

JAYARAM COLLEGE OF ENGINEERING AND TECHNOLOGY Pagalavadi, Tiruchirappalli (An approved by AICTE and Affiliated to Anna University)

CMSC 202 Section 010x Spring Justin Martineau, Tuesday 11:30am

Paytm Programming Sample paper: 1) A copy constructor is called. a. when an object is returned by value

C++: Const Function Overloading Constructors and Destructors Enumerations Assertions

Transcription:

1 Introduction Structured bindings with polymorphic lambas Aaryaman Sagar (aary800@gmail.com) August 14, 2017 This paper proposes usage of structured bindings with polymorphic lambdas, adding them to another place where auto can be used as a declarator std :: for_each (map, []( auto [key, value ]) { cout << key << " " << value << endl ; ); This would make for nice syntactic sugar to situations such as the above without having to decompose the tuple-like object manually, similar to how structured bindings are used in range based for loops for ( auto [key, value ] : map ) { cout << key << " " << value << endl ; 2 Motivation 2.1 Simplicity and uniformity Structured binding initialization can be used almost anywhere auto is used to initialize a variable (not considering auto deduced return types), and allowing this to happen in polymorphic lambdas would make code simpler, easier to read and generalize better std :: find_if ( range, []( const auto & [key, value ]) { return examine ( key, value ); ); 2.2 Programmer demand There is some programmer demand and uniform agreement on this feature 1. Stack Overflow: Can the structured bindings syntax be used in polymorphic lambdas 2. ISO C++ : Structured bindings and polymorphic lambdas 2.3 Prevalence It is not uncommon to execute algorithms on containers that contain a value type that is either a tuple or a tuple-like decomposable class. And in such cases code usually deteriorates to manually unpacking the instance of the decomposable class for maximum reaadability, for example return std :: when_all (one, two ). then ([]( auto futures ) { auto & one = std :: get <0 >( futures ); auto & two = std :: get <1 >( futures ); ); return one. get () * two. get (); 1

Structured bindings with polymorphic lambas 2 The first two lines in the lambda are just noise and can nicely be replaced with structured bindings in the function parameter return std :: when_all (one, two ). then ([]( auto [one, two ]) { return one. get () * two. get (); ); 3 Impact on the standard The proposal describes a pure language extension which is a non-breaking change - code that was previously ill-formed now would have well-defined semantics. 4 Interaction with concepts and traits Definition (x-decomposable) For a given domain of types that are decomposable, if a type can be decomposed into a structured binding expression with x bindings, then it is said to be x-decomposable (reference [dcl.struct.bind] for the exact requirements). More specifically, if the following expression is well formed in the least restrictive member access scope (where privates are accessible) for a type T auto && [one, two, three,, x] = o; Where decltype(o) is T, then the type T is said to be x-decomposable This can be made available to the compiler as both a concept and a trait. The presence of such a concept makes it easy to define templates in terms of a type that is x-decomposable. A trait allows for the same thing but can be considered more versatile as it also fits well with existing code that employs value driven template specialization mechanisms and other more complicated specialization workflows. It is possible to make a concept or trait that enables us to check if a type is decomposable into x bindings by virtue of it s interface. In particular the presence of an ADL defined or member get<>() function and the existence of specialized std::tuple element<> and std::tuple size<> traits qualifies something to be x-decomposable. However, a type can be x-decomposable even when these are not present (see [dcl.struct.bind]p4) [dcl.struct.bind]p2 and [dcl.struct.bind]p3 define decomposability that can be checked by the programmer at compile time (described above) via a concept or trait. However [dcl.struct.bind]p4 describes a method of unpacking that cannot be enforced purely by the language constructs available as of C++17. As such something like a compiler intrinsic, say is decomposable<t, x> is required. Given support from a compiler with such an intrinsic, defining a trait and concept that check if a type is x-decomposable on top of that is trivial. The trait itself can be used as a backend for the concept, leaving the implementation of the concept entirely in portable code without the help of compiler intrinsics. The concept, say std::decomposable<x> accepts a non-type template parameter of type std::size t that determines the cardinality of the structured bindings decomposition. This concept holds if a type is x-decomposable (and this will take into consideration the requirements set forth by [dcl.struct.bind] paragraphs 2, 3 and 4. The corresponding trait, say std::is decomposable<t, x> inherits from std::integral constant<bool, true> if and only if type T is x-decomposable. The usual variable template std::is decomposable v<t, x> should also be defined. 5 Impact on overloading and function resolution Lambdas do not natively support function overloading, however one can lay out lambdas in a way that they are overloaded, for example let s assume the following definition of make overload() for the rest of the paper template < typename Types > class Overload : public Types { template < typename T> Overload (T && types ) : Types { std :: forward <T >( types ) { 2

Structured bindings with polymorphic lambas 3 using Types :: operator (); ; template < typename Types > auto make_ overload ( Types && instances ) { return Overload < std :: decay_t < Types > >{ std :: forward < Types >( instances ); Now this can be used like so to generate a functor with overloaded operator() methods from anonymous lambdas namespace { auto one = []( int ) {; auto two = []( char ) {; auto overloaded = make_ overload ( one, two ); // namespace < anonymous > In such a situation the consequences of this proposal must be considered. The easiest way to understand this proposal is to consider the rough syntactic sugar that this provides. A polymorphic lambda with a structured binding declaration translates to a simple functor with a templated operator() method with the structured binding decomposition happening inside the function auto lambda = []( const auto [key, value ]) { ; * Expansion of the above lambda class ANONYMOUS_ LAMBDA { template < std :: decomposable <2 > Type > auto operator ()( const Type instance ) const { auto && [key, value ] = std :: move ( instance ); ; The std::move() is added to force a conversion to xvalue type because the expression e (see [dcl.struct.bind]p1) is not an lvalue in the structured binding declaration, and when e is not an lvalue, the introduced bindings are decomposed as if e was an xvalue. If e was an lvalue, (i.e. if & was used as the ref-qualifier or if && was used and an lvalue was passed in to the lambda) then the std::move() will be omitted (see the next expansion for an example where std::move() is omitted) Similarly a lambda that has two seperate groups of structured binding declarations will translate with the decompositions happening serially within the function body in order of binding declarations from left to right auto lambda = []( const auto [key, value ], auto & [one, two, three ]) { ; * Expansion of the above lambda class ANONYMOUS_ LAMBDA { template < std :: decomposable <2 > One, std :: decomposable <3 > Two > auto operator ()( const One one, Two & two ) { auto && [key, value ] = std :: move ( one ); auto & [one, two, three ] = two ; ; Given the above expansions, a polymorphic lambda behaves almost identically to a lambda with a auto parameter type with the difference that these are constrained to work only with parameters that are x-decomposable. And nothing special happens when overloading namespace { auto one = []( int ) {; 3

Structured bindings with polymorphic lambas 4 auto two = []( auto [key, value ]) {; auto overloaded = make_ overload ( one, two ); // namespace < anonymous > int main () { auto integer = int {1; auto pair = std :: make_ pair (1, 2); auto error = double {1; // calls the lambda named " one " overloaded ( integer ); // calls the lamdba named " two " overloaded ( pair ); // error overloaded ( error ); 5.1 Viable orthogonal overloads One key point to consider here is that the concept based constraints on such lambdas allows for the following two orthogonal overloads to work nicely with each other namespace { auto lambda_one = []( auto [one, two ]) {; auto lambda_ two = []( auto [ one, two, three ]) {; auto overloaded = make_ overload ( lambda_one, lambda_ two ); // namespace < anonymous > int main () { auto tup_one = std :: make_ tuple (1, 2); auto tup_two = std :: make_ tuple (1, 2, 2); overloaded ( tup_one ); overloaded ( tup_two ); return 0; Since here either one lambda can be called or both, in no case can both satisfy the requirements set forth by the compiler concept std::decomposable<x> 5.2 Access control and decompositions Another key point to consider is acess control within the expansion of the lambda. Decompositions will share the access control powers of the code in the surrounding scope where the lambda is defined. So if the decomposition was in the body of the lambda and was valid (for example, even if the type being decomposed has private get<>() methods) the lambda would be able to decompose it successfully. For example the following code is valid class Something { static auto make_ decomposer (); private : std :: tuple <int, int > tup {1, 2; template < std :: size_t Index > int get (); ; namespace std { template <> class tuple_size < Something > : public std :: integral_ constant < std :: size_t, 2> {; 4

Structured bindings with polymorphic lambas 5 template < std :: size_t Index > class tuple_element < Index, Something > { using type = int ; ; // namespace std template < std :: size_t Index > int Something :: get () { return std :: get < Index >( this -> tup ); auto Something :: make_ decomposer () { // decomposition of Something instances is allowed access to the privates // of Something since it s defined in a context where private members are // visible return []( auto [one, two ]) { assert ( one == 1); assert ( two == 2); ; void foo () { auto something = Something {; auto decomposer = Something :: make_ decomposer (); // the decomposition here happens in the scope of the lambda so is valid decomposer ( something ); 6 Compatibility with ODR A typical problem with traits classes comes from the ODR rule. We have the same potential problem with the trait defined earlier - std::is deomposable<>. A type may be defined such that it is unable to be decomposed at one point in a program and might be decomposable at other points. This problem comes from the nature of the structured bindings feature. To maintain backwards compatibility with types that were defined before the feature, structured bindings use traits types to detect whether types are decomposable. In particular one trait that is always used and needs to be defined for non array and non class types with only public members is std::tuple size. By virtue of being a trait, it needs to be defined or specialized after a class s definition. This leads to possible discrepencies in whether it is complete or not at different points in the program. And if we instantiate the std::is decomposable trait at these incompatible points we risk violating ODR. There are three possible solutions to this problem 6.1 Disabled overloading based on decomposability Disabling overloading based on decomposability is another solution to the problem of possibly violating ODR. With this we can treat all lambdas with structured bindings parameters as the normal equivalent lambda with a single non structured binding parameter with no decomposing and the same cv-ref qualifications. For example auto one = []( auto one, auto && [ two, three ], auto four, const auto & [ five ]) {; Would be equivalent to auto one = []( auto one, auto && two, auto three, const auto & four ) This seems to be the simplest way forward. As a corrolary std::is decomposable can still safely exist and be used in situations where programmers are guaranteed to get a decomposable type using the approach described in the next subsection 5

Structured bindings with polymorphic lambas 6 6.2 Poisoning the trait One possible approach is to poison the std::is decomposable trait to produce ill defined code when instantiated with a type that is not decomposable (i.e. a non-array, non-class type with only public data members and a type for which the required structured bindings specializations are not defined). This would work in concept something like this template < typename T, typename = void_t <>> class IsDecomposableImpl { * This would render the program ill formed when instantied with a non * decomposable type - including but not limited to types which do not * have std :: tuple_ size defined static_ assert ( always_false <T >); ; * Then the other specializations template < typename T > class IsDecomposableImpl <T,. > { ; This would limit the overloadability of lambdas which accept structured bindings parameters to work only with other lambdas accepting structured bindings parameters. 6.3 Argument Introspection Like function arguments, programers should be able to detect the number of bindings to the structured bindings parameters in a lambda. So they can employ metaprogramming strategies. To enable this, there can be traits that list the number of function arguments allowed, which ones are structured bindings parameters, the types and cv qualifications of each function parameter (template types should be treated specially) 7 Conversions to function pointers A capture-less polymorphic lambda with structured binding parameters can also be converted to a plain function pointer. Just like a regular polymorphic lambda using FPtr_t = void (*) ( std :: tuple < int, int >); auto f_ptr = static_cast < FPtr_t >([]( auto [a, b ]){); So another conversion operator needs to be added to the expansion of the polymorphic lambda with structured bindings above auto lambda = []( const auto [one, two ]) { ; * Expansion of the above lambda in C ++17 form with respect to overloading class ANONYMOUS_ LAMBDA { template < std :: decomposable <2 > Type > auto operator ()( const Type instance ) const { auto && [one, two ] = std :: move ( instance ); private : 6

Structured bindings with polymorphic lambas 7 * Cannot use operator () because that will either cause moves or copies, * elision isn t guaranteed to happen to function parameters ( even in * return values ) template < std :: decomposable <2 > Type > static auto invoke ( const Type instance ) { auto && [key, value ] = std :: move ( instance ); * Enforce the decomposable requirement on the argument of the function template < typename Return, std :: decomposable <2 > Arg > operator Return (*)( Arg )() const { return & invoke ; ; And like regular polymorphic lambdas, returning the address of the static function invokes an instantiation of the function with the types used in the conversion operator If in order to avoid ODR all together, the concept is avoided, then the concept should be avoided in the conversion operators to function pointers as well. In which case the conversion operators would be the same just without the concept constraint. 8 Exceptions Any exceptions during copy/move construction of the instance which is to be decomposed being will be thrown from the call site, just as with regular polymorphic lambdas. However, if there is an exception thrown during the decomposition process, for example if get<>() throws, that will propagate from within the lambda. So if function level try catch blocks were allowed for lambdas, those would catch any exceptions generated during the decomposition process, whereas exceptions from the copy/move construction for the creation of the entity e (see [dcl.struct.bind]p1) would not be caught by the imaginary function level try-catch block. 9 Acknowledgements Thanks to Eric Niebler and Nicol Bolas for the suggestions and the help with this paper! 10 Changes to the current C++17 standard 10.1 Section 8.1.5.1 ([expr.prim.lambda.closure]) paragraph 3 For a generic lambda, the closure type has a public inline function call operator member template (17.5.2) whose templateparameter-list consists of one invented type template-parameter for each occurrence of auto in the lambdas parameterdeclaration-clause, in order of appearance. For each occurrence of a structured binding with cardinality x, the templateparameter-list consists of an invented type template-parameter with the constraint that it has to be decomposable into x structured bindings (see [dcl.struct.bind]). And as such the function template only participates in overloading when all the structured bindings are appropriately decomposable. The invented type template-parameter is a parameter pack if the corresponding parameter-declaration declares a function parameter pack (11.3.5). A structured binding parameter-declaration cannot be used to invent a parameter pack. The return type and function parameters of the function call operator template are derived from the lambda-expressions trailing-return-type and parameter-declaration-clause by replacing each occurrence of auto in the decl-specifiers of the parameter-declaration-clause with the name of the corresponding invented template-parameter. 7

Structured bindings with polymorphic lambas 8 10.2 Section 11.5 ([dcl.struct.bind]) 5. If the number of structured bindings introduced by a structured binding declaration is x, then a type T is called x-decomposable if the following is well formed in the least restrictive member access scope (where privates are accessible) auto && [one, two, three,, x] = o; where T is decltype(o) 10.3 Section 23.15.4.3 Type properties ([meta.unary.prop]) template < typename T, std :: size_t X > class is_ decomposable ; Condition The type T has to be x-decomposable (see [dcl.struct.bind]p5) 8