Ref-qualified member functions

    In this post I will talk about a new and (it seems to me) relatively little-known feature of C ++ - reference-qualified member functions . I’ll tell you about the rules for overloading such functions, and also, as an example of use, I’ll tell you how to use ref-qualifie d functions to try to improve the resource management scheme implemented using another C ++ idiom - RAII .

    Introduction


    So, recently, in C ++, it has become possible to qualify member functions with a link (at least in appearance it looks like a link). These qualification marks can be lvalue , rvalue references, can be combined with const qualification.

    class some_type
    {
      void foo() & ; 
      void foo() && ;
      void foo() const & ;
      void foo() const && ;
    };
    


    Why is this needed?


    Strictly speaking, officially this feature is called a little differently, namely “ref-qualifiers for * this” or “rvalue references for * this” . But it seems to me that this name is a little confusing, as it may seem that the object changes type when calling functions with different qualifications. Actually, the * this type never changes. So what's the trick? And the trick is that thanks to these qualifiers it becomes possible to overload member functions by the context (rvalue, lvalue, etc) in which the object is used.

    int main()
    {
      some_type t; 
      t.foo(); // some_type::foo() & 
      some_type().foo(); // some_type::foo() && 
    }
    


    How it works?


    To begin with, in C ++ there has long been a mechanism for resolving overloads between member functions and free functions. Why do you need it, you ask, because you can understand whether a free function or a class method is called at least externally, according to the syntax, in one case obj.f () , in the other just f () ? The fact is that when it comes to operator overloading, there may no longer be differences in syntax. for instance

    struct some_type
    {
      bool operator == (int) const; 
    };
    bool operator == (const some_type& l, long r); 
    void g()
    {
      some_type t;
      int i = 42;
      t == i; // Какую функцию вызвать?
    }
    


    To resolve such an overload, the compiler presented a member function as a free function with an additional parameter - a reference to the object at which the function is being called and then allowed overloading among all free functions. So to implement the innovation, it was only necessary to “twist” already existing behavior a little, namely to create different signatures of the overload candidates for variously qualified member functions.
    I’ll say a few words about how this mechanism specifically works, because it is far from always obvious which function is the best candidate for overloading in a particular case. Consider again the code from the first example.

    class some_type
    {
      void foo() & ; // 1
      void foo() && ; // 2
      void foo() const & ; // 3
      void foo() const && ; // 4
    };
    void g()
    {
      some_type().foo();
    }
    


    For this call, 3 candidates are suitable: 2, 3, and 4. To resolve between them, the standard has special rules that look rather verbose and complex on paper, but the essence of which is to select a function that most closely matches the type.
    I will try to retell the chain of reasoning for the conclusion of the candidate, as I imagine it. In this example, the expression some_type () is an rvalue . Potentially, functions 2, 3, or 4 can be called. But rvalue reference qualified functions are more “matched” to the type of the original expression (rvalue) than const & . There are options 2 and 4. In the fourth option, for full compliance, you need to do an additional action on the original type - addconst , while in the second embodiment no additional actions are required. Therefore, in the end, option 2 will be selected.

    How to use?


    It is obviously convenient to use this innovation in those cases when the behavior of an object should differ from the contexts in which it is used. For example, we can make it safer to use a pointer to a stored resource when using RAII.

    class file_wrapper
    {
    public:
    	// ...
      operator FILE* () {return held_;}
      ~file_wrapper() {fclose(held_);}
    private:
      FILE* held_;
    };
    


    In this example, operator FILE * () represents a huge hole in the safe use of the file wrapper.
    Imagine this context of use:

    FILE* f = file_wrapper("some_file.txt", "r");
    // Работа с f
    


    Now we have the opportunity to make this, in fact, very convenient, function more (but not completely) safe.

    operator FILE* () & {return held_;} // Можно вызвать только у lvalue объектов
    


    You can look at RAII from a slightly different side. Since we can now “understand” what they call us in different contexts, let's just transfer ownership of the resource instead of copying in cases where our object will no longer be used.

    template 
    class some_type
    { 
    public:
      operator std::unique_ptr() const &
      {
        return std::unique_ptr(new T(*held_)); // Копируем
      } 
      operator std::unique_ptr() &&
      { 
        return std::move(held_); // Отдаем владение
      } 
    private:
      std::unique_ptr held_;
    };
    some_type f();
    void g()
    {
      std::unique_ptr p = f();
    }
    

    Also popular now: