Template-based implementation of Sutter's exception-safe operator= idiom

Discussion in 'C++' started by Mikhail N. Kupchik, Aug 19, 2004.

  1. Hi All.

    I have a question regarding Herb Sutter's idiom of implementation of
    operator= via nonthrowing swap() member function, to guarantee strict
    exception safety.

    The idea of the idiom is shown by the example:

    -- code fragment 1 ----------------------------------------------------
    class MyClass
    {
    ...
    public:
    void swap( MyClass& ) throw();

    MyClass& operator=( const MyClass& rvalue )
    {
    MyClass other( rvalue ); // (1)
    swap( other );
    return *this;
    }
    };
    -- end of code fragment 1 ---------------------------------------------

    Thus we need to implement only copy constructor (used by (1)) and get
    strict exception safety: every possible exception in operator= is
    produced by (1) and gets thrown out before `this' object is actually
    modified.

    My question is the following: is it possible to implement the idiom of
    exception-safe operator= via template class (with little help of CRTP
    of course), so that I could write for many of my classes:

    -- code fragment 2 ----------------------------------------------------
    class MyClass
    : public CanonicalReplacement< MyClass >
    {
    ...
    public:
    void swap( MyClass& ) throw();
    };
    -- end of code fragment 2 ---------------------------------------------

    My first attempt was

    -- code fragment 3 ----------------------------------------------------
    template< typename T >
    struct CanonicalReplacement
    {
    T& operator=( const T& other )
    {
    if( this != &other ) // (2)
    {
    T temp_object( other ); // (3)
    T::swap( temp_object ); // (4)
    }
    return *this;
    }
    };
    -- end of code fragment 3 ---------------------------------------------

    Besides of one slippery thing that in (2) `this' is implicitly upcasted
    to CanonicalReplacement<T>* and actually pointers to CanonicalReplacement<T>
    rather than T are compared (I'm in doubt if most compilers can optimize
    all pointer arithmetic that arises here), there is one bigger trouble:
    the code simply does not work, as the operator= is actually declared for
    argument types ( CanonicalReplacement<T>&, const T& ), not ( T&, const T& );
    thus, compiler generates default version of it for class T.

    The following example illustrates it:

    -- code fragment 4 ----------------------------------------------------
    struct A
    {
    A& operator=( const A& ) { cout << "operator= of A\n"; }
    };

    struct B
    : public CanonicalReplacement< A >
    {
    A a_;

    B() { }
    B( const B& ) { cout << "cctor of B\n"; }
    void swap() throw() { cout << "swap of B\n"; }
    };

    int main()
    {
    B() = B(); // (5)
    };
    -- end of code fragment 4 ---------------------------------------------

    In (5), if CanonicalReplacement<B>::eek:perator= is called, it will print
    `cctor of B' in (3) then `swap of B' in (4).
    But actually default B::eek:perator= is generated, it calls A::eek:perator= for
    `a_' field and prints `operator= of A'.

    Unfortunately it is not possible to use Barton-Nackman method of restricted
    template expansion with aid of friend member function (see code fragment 5),
    because operator= must be a member function (according to section 13.5.3.1
    of the ISO standard for C++ programming language).

    -- code fragment 5 ----------------------------------------------------
    template< typename T >
    struct CanonicalReplacement
    {
    friend T& operator=( T& lvalue, const T& rvalue )
    {
    if( &lvalue != &rvalue )
    {
    T temp_object( rvalue );
    lvalue.swap( temp_object );
    }
    return lvalue;
    }
    };
    -- end of code fragment 5 ---------------------------------------------

    The idion can be implemented via preprocessor, but this is a hack of course.
    Does anybody have any other ideas on this?

    -- Mikhail Kupchik

    [ See http://www.gotw.ca/resources/clcm.htm for info about ]
    [ comp.lang.c++.moderated. First time posters: Do this! ]
    Mikhail N. Kupchik, Aug 19, 2004
    #1
    1. Advertising

  2. Fix: of course I mean

    struct B
    : public CanonicalReplacement< B >

    in code fragment 4.

    -- Mikhail Kupchik

    [ See http://www.gotw.ca/resources/clcm.htm for info about ]
    [ comp.lang.c++.moderated. First time posters: Do this! ]
    Mikhail N. Kupchik, Aug 20, 2004
    #2
    1. Advertising

  3. (Mikhail N. Kupchik) writes:

    > Hi All.
    >
    > I have a question regarding Herb Sutter's idiom of implementation of
    > operator= via nonthrowing swap() member function, to guarantee strict
    > exception safety.


    Herb doesn't deserve the blame for that technique (though maybe for
    recommending it a bit too heartily). The swapping assignment operator
    is almost always a bad idea, especially in a template, because it
    spends cycles on the strong guarantee at what may be the wrong level
    of granularity.

    (http://lists.boost.org/MailArchives/boost/msg36928.php)

    Any time you make assignment give the strong guarantee by copying and
    swapping, you force anyone who wants to use assignment in an operation
    which doesn't need that strong guarantee to pay for the unneccessary
    copy, which could be very expensive.

    --
    Dave Abrahams
    Boost Consulting
    http://www.boost-consulting.com

    [ See http://www.gotw.ca/resources/clcm.htm for info about ]
    [ comp.lang.c++.moderated. First time posters: Do this! ]
    David Abrahams, Aug 20, 2004
    #3
  4. Mikhail N. Kupchik

    James Hopkin Guest

    Oops: in my other reply I left in a paragraph about std::swap, which I
    meant to remove.

    Default std::swap is definitely *not* what we want, as that will
    recursively call the assignment operator until the end of time.

    James

    [ See http://www.gotw.ca/resources/clcm.htm for info about ]
    [ comp.lang.c++.moderated. First time posters: Do this! ]
    James Hopkin, Aug 20, 2004
    #4
  5. Mikhail N. Kupchik

    James Hopkin Guest

    (Mikhail N. Kupchik) wrote in message news:<>...
    >
    > Does anybody have any other ideas on this?
    >


    I'm not sure I'd want to do this (reasons listed below), but I can
    suggest an implementation.

    // T requirements: no-throw swap(T&, T&)
    // no copy assignment operator defined

    template< typename T >
    struct CanonicalReplacement
    {

    BOOST_STATIC_ASSERT(boost::is_base_and_derived<CanonicalReplacement,
    T>::value);

    CanonicalReplacement& operator=(const CanonicalReplacement&
    other)
    {
    T& derived_this = static_cast<T&> (*this);
    const T& derived_other = static_cast<const T&>(other);

    if(&derived_this != &derived_other) // this line is an
    optimisation only
    {
    T temp(derived_other);

    swap(derived_this, temp);
    }
    return *this;
    }
    };

    This can be *privately* inherited, since assignment operators don't
    get inherited anyway.

    I used a namespace-scope swap, because this is more general. You can
    always write a global swap to call a member one.

    I'm calling a global std::swap, since this is more general (if you
    want a member version to be used, you can define a global swap which
    calls the member one).

    To be extra safe, you can use boost::address_of rather than the &
    operator, just in case the built-in & has been overridden.


    As for why I wouldn't do this:

    1) Assignment by swapping with temporary is a well-known idiom
    (largely thanks to Herb - correct me if I'm wrong). Most coders will
    immediately understand an assignment operator written this away, but
    may puzzle for a while seeing this base. Of course, that problem goes
    away if it were to become an accepted idiom (chicken and egg
    situation).

    2) Ease of mis-use: if the client defines a copy assignment
    operator in T or any derived class without removing this base, things
    go silently wrong. I can't think of a way of preventing this without
    overhead.

    3) Personal taste: I wouldn't want to use a base class to do
    something so simple.

    4) Potential inheritance overheads: with ideal compilers this
    method would always be zero overhead, but in the real world we know
    that's not always the case, particularly if you needed to inherit
    other classes.

    5) Compilation time: I would imagine instantiating this template
    class everywhere would have some impact on compile times.


    Cheers,
    James

    [ See http://www.gotw.ca/resources/clcm.htm for info about ]
    [ comp.lang.c++.moderated. First time posters: Do this! ]
    James Hopkin, Aug 20, 2004
    #5
  6. David Abrahams <> writes:

    > Any time you make assignment give the strong guarantee by copying
    > and swapping, you force anyone who wants to use assignment in an
    > operation which doesn't need that strong guarantee to pay for the
    > unneccessary copy, which could be very expensive.


    For those who do need the strong guarantee at a higher level, how do
    they go about getting it? I understand the "SGI argument" about
    concurrency control, but I'm missing the analogous option here to add
    exception-safe "locks" at a higher level.

    Take your example where a client wants an assignment followed by
    push_back() to have the strong guarantee.¹ Is this a potential
    solution?


    // Neither compiled nor tested.

    template <typename C, typename T>
    C& safe_assign_push(C& dest, C const& src, T const& val)
    {
    C temp( src );
    temp.push_back( val );
    dest.swap( temp );
    return dest;
    }


    Footnotes:
    ¹ http://lists.boost.org/MailArchives/boost/msg36928.php

    --
    Steven E. Harris

    [ See http://www.gotw.ca/resources/clcm.htm for info about ]
    [ comp.lang.c++.moderated. First time posters: Do this! ]
    Steven E. Harris, Aug 21, 2004
    #6
  7. (James Hopkin) wrote in message news:<>...
    > (Mikhail N. Kupchik) wrote in message news:<>...
    > >
    > > Does anybody have any other ideas on this?
    > >

    >
    > I'm not sure I'd want to do this (reasons listed below), but I can
    > suggest an implementation.


    Hi.

    Default operator= in T calls operator= for all base classes and fields, not only
    for CanonicalReplacement< T >.

    The program below

    -- code fragment 6 ----------------------------------------------------

    template< typename T >
    struct CanonicalReplacement
    {
    CanonicalReplacement& operator=(const CanonicalReplacement& other)
    {
    T& derived_this = static_cast<T&> (*this);
    const T& derived_other = static_cast<const T&>(other);

    if(&derived_this != &derived_other) // this line is an optimisation only
    {
    T temp(derived_other);

    derived_this.swap(temp);
    }
    return *this;
    }
    };

    struct A
    {
    A& operator=( const A& ) { cout << "A::eek:perator= (should not be called)\n"; }
    };

    struct B
    : public CanonicalReplacement< B >
    {
    A a_;
    B() { }
    void swap( B& ) throw() { cout << "B::swap()\n"; }
    B( const B& ) { cout << "cctor of B\n"; }
    };

    int main()
    {
    B() = B();
    }

    -- code fragment 6 ----------------------------------------------------

    prints

    cctor of B
    B::swap()
    A::eek:perator= (should not be called)

    -- Mikhail Kupchik

    [ See http://www.gotw.ca/resources/clcm.htm for info about ]
    [ comp.lang.c++.moderated. First time posters: Do this! ]
    Mikhail N. Kupchik, Aug 21, 2004
    #7
  8. Mikhail N. Kupchik

    Glen Low Guest

    > In (5), if CanonicalReplacement<B>::eek:perator= is called, it will print
    > `cctor of B' in (3) then `swap of B' in (4).
    > But actually default B::eek:perator= is generated, it calls A::eek:perator= for
    > `a_' field and prints `operator= of A'.


    The problem is copy constructors are not inherited; it only seems that
    way because if you don't define a copy constructor, the compiler tries
    to chain the superclass one with the ones for each member.

    You can kludgify it by creating an Replace member in
    CanonicalReplacement<B> to do the copy and swap idiom, then calling
    this from the defined copy constructor in B. Or even make Replace a
    function template like std::swap is.

    The only other thing I can think of (which is subtly icky on other
    levels) is to reverse the inheritance:

    1. Put all the core B stuff into B_core.
    2. Make B inherit from CanonicalReplacement<B_core>, and ensure B
    itself has no member variables etc.
    3. Regularize your other constructors (that's the icky part).

    Cheers,
    Glen Low, Pixelglow Software
    www.pixelglow.com

    [ See http://www.gotw.ca/resources/clcm.htm for info about ]
    [ comp.lang.c++.moderated. First time posters: Do this! ]
    Glen Low, Aug 23, 2004
    #8
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Steven T. Hatton

    Sutter's Pimples: Good, Bad, or Ugly?

    Steven T. Hatton, Apr 11, 2004, in forum: C++
    Replies:
    24
    Views:
    1,073
    Steven T. Hatton
    Apr 12, 2004
  2. Debajit  Adhikary
    Replies:
    2
    Views:
    2,015
    Christopher Benson-Manica
    Jul 15, 2004
  3. puzzlecracker
    Replies:
    1
    Views:
    270
    Victor Bazarov
    Aug 9, 2005
  4. puzzlecracker

    The Trouble With Locks by sutter

    puzzlecracker, Dec 25, 2005, in forum: C++
    Replies:
    2
    Views:
    300
    mlimber
    Dec 27, 2005
  5. Noah Roberts

    Error in Sutter book?

    Noah Roberts, Nov 15, 2006, in forum: C++
    Replies:
    24
    Views:
    814
Loading...

Share This Page