...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
Call Policies |
In C++, we often deal with arguments and return types such as pointers and references. Such primitive types are rather, ummmm, low level and they really don't tell us much. At the very least, we don't know the owner of the pointer or the referenced object. No wonder languages such as Java and Python never deal with such low level entities. In C++, it's usually considered a good practice to use smart pointers which exactly describe ownership semantics. Still, even good C++ interfaces use raw references and pointers sometimes, so Boost.Python must deal with them. To do this, it may need your help. Consider the following C++ function:
X& f(Y& y, Z* z);
How should the library wrap this function? A naive approach builds a Python X object around result reference. This strategy might or might not work out. Here's an example where it didn't
>>> x = f(y, z) ##x refers to some C++ X
>>> del y
>>> x.some_method() ##CRASH!
What's the problem?
Well, what if f() was implemented as shown below:
X& f(Y& y, Z* z)
{
y.z = z;
return y.x;
}
The problem is that the lifetime of result X& is tied to the lifetime of y, because the f() returns a reference to a member of the y object. This idiom is is not uncommon and perfectly acceptable in the context of C++. However, Python users should not be able to crash the system just by using our C++ interface. In this case deleting y will invalidate the reference to X. We have a dangling reference.
Here's what's happening:
We could copy result into a new object:
>>> f(y, z).set(42) ##Result disappears
>>> y.x.get() ##No crash, but still bad
3.14
This is not really our intent of our C++ interface. We've broken our promise that the Python interface should reflect the C++ interface as closely as possible.
Our problems do not end there. Suppose Y is implemented as follows:
struct Y
{
X x; Z* z;
int z_value() { return z->value(); }
};
Notice that the data member z is held by class Y using a raw pointer. Now we have a potential dangling pointer problem inside Y:
>>> x = f(y, z) ##y refers to z
>>> del z ##Kill the z object
>>> y.z_value() ##CRASH!
For reference, here's the implementation of f again:
X& f(Y& y, Z* z)
{
y.z = z;
return y.x;
}
Here's what's happening:
Call Policies may be used in situations such as the example detailed above. In our example, return_internal_reference and with_custodian_and_ward are our friends:
def("f", f,
return_internal_reference<1,
with_custodian_and_ward<1, 2> >());
What are the 1 and 2 parameters, you ask?
return_internal_reference<1
Informs Boost.Python that the first argument, in our case Y& y, is the owner of the returned reference: X&. The "1" simply specifies the first argument. In short: "return an internal reference X& owned by the 1st argument Y& y".
with_custodian_and_ward<1, 2>
Informs Boost.Python that the lifetime of the argument indicated by ward (i.e. the 2nd argument: Z* z) is dependent on the lifetime of the argument indicated by custodian (i.e. the 1st argument: Y& y).
It is also important to note that we have defined two policies above. Two or more policies can be composed by chaining. Here's the general syntax:
policy1<args...,
policy2<args...,
policy3<args...> > >
Here is the list of predefined call policies. A complete reference detailing these can be found here.
Remember the Zen, Luke: "Explicit is better than implicit" "In the face of ambiguity, refuse the temptation to guess" |
Copyright © 2002-2003 David Abrahams
Copyright © 2002-2003 Joel de Guzman
Permission to copy, use, modify, sell and distribute this document
is granted provided this copyright notice appears in all copies. This document
is provided "as is" without express or implied warranty, and with
no claim as to its suitability for any purpose.