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

M

Mikhail N. Kupchik

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
 
M

Mikhail N. Kupchik

Fix: of course I mean

struct B
: public CanonicalReplacement< B >

in code fragment 4.

-- Mikhail Kupchik
 
D

David Abrahams

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

James Hopkin

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
 
J

James Hopkin

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
 
S

Steven E. Harris

David Abrahams said:
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
 
M

Mikhail N. Kupchik

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
 
G

Glen Low

In (5) said:
`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
 

Ask a Question

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

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,483
Members
44,901
Latest member
Noble71S45

Latest Threads

Top