Search code examples
pythonc++swig

SWIG: avoid C++-to-Python aliasing


I am building a library in C++ which is interfaced to Python 3 using SWIG 4.0.1. After some time developing I noticed a problem with (what I think is called) aliasing. I prepared a minimal example which this also happens.

Consider the class dummy with a single (private) attribute attr which is initialized at 0. Now, I have this really small python script:

import dummy_wrap
d1 = dummy_wrap.dummy()
d2 = d1
d1.set_attr(12)
print(d2.get_attr()) # this prints '12', not '0'

I hope that what I call aliasing is what is really happening here: object d2 is never modified, yet it takes the value given to d1, as if the assignment d2=d1 was actually pointer assignment. What I would like to happen (and also what I expect to happen) is that the assignment operator a=b makes a hard copy of b passes it onto a. In other words, modifications to a should not affect b and viceversa, as long as this meets the implementation details (as is the case in my library and in the minimal example).

As a final note, this is the messages I get when compiling:

g++ -fPIC -c dummy.cpp
g++ -fPIC -shared -o libdummy_lib.so dummy.o
swig -Wall -c++ -python -py3 -o dummy_wrap.cxx dummy_wrap.i
dummy.hpp:10: Warning 362: operator= ignored
dummy.hpp:11: Warning 362: operator= ignored
dummy.hpp:7: Warning 509: Overloaded method dummy::dummy(dummy &&) effectively ignored,
dummy.hpp:6: Warning 509: as it is shadowed by dummy::dummy(dummy const &).
g++ -fPIC -c dummy_wrap.cxx -I /usr/include/python3.8
g++ -fPIC -shared -o _dummy_wrap.so dummy_wrap.o -L . -ldummy_lib -lpython3.8

Questions:

  • I would like to understand what is happening here and why: do the dummy(dummy&&) and dummy& operator=(dummy&&) affect how SWIG wraps dummy(const dummy&) and dummy& operator=(const dummy&)?
  • Is it reasonable to expect what I would like to have as default behavior, namely, is this (aliasing) the expected behavior of the class after interfacing to python using SWIG?
  • How can I fix this: what changes should I make so that the operator = makes a hard copy?

Thanks a lot in advance.

In order to replicate this, the class header dummy.hpp is:

#pragma once

class dummy {
    public:
        dummy() = default;
        dummy(const dummy&) = default;
        dummy(dummy&&) = default;
        ~dummy() = default;
        dummy& operator=(const dummy&) = default;
        dummy& operator=(dummy&&) = default;
        void set_attr(int v);
        int get_attr() const;
    private:
        int attr = 0;
};

and the class implementation dummy.cpp is simply

#include "dummy.hpp"
void dummy::set_attr(int v) { attr = v; }
int dummy::get_attr() const { return attr; }

the interface file dummy_wrap.i is:

%module dummy_wrap
%{
#include "dummy.hpp"
%}
%include "dummy.hpp"

and the Makefile I use to compile and link everything is

all: libdummy_lib.so _dummy_wrap.so
_dummy_wrap.so: dummy_wrap.o
    g++ -fPIC -shared -o _dummy_wrap.so dummy_wrap.o -L . -ldummy_lib -lpython3.8
dummy_wrap.o: dummy_wrap.cxx
    g++ -std=c++17 -fPIC -c dummy_wrap.cxx -I /usr/include/python3.8
dummy_wrap.cxx: dummy_wrap.i
    swig -Wall -c++ -python -py3 -o dummy_wrap.cxx dummy_wrap.i
libdummy_lib.so: dummy.o
    g++ -fPIC -shared -o libdummy_lib.so dummy.o
dummy.o: dummy.cpp dummy.hpp
    g++ -std=c++17 -fPIC -c dummy.cpp
clean:
    rm -f libdummy_lib.so dummy.o
    rm -f _dummy_wrap.so dummy_wrap.o dummy_wrap.cxx dummy_wrap.py

In linux, remember to update the LD_LIBRARY_PATH using:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.

Solution

  • So, thanks to a few comments in the original post, a possible solution is to add a clone() method. One could do this by simply extending the C++ header. But, since this may seem weird to some people as in many cases a clone() method in C++ is not necessary at all, we can extend the python class using SWIG. Simply take the dummy_wrap.i in the example and add, at the end of the file, the following piece of code

    %extend dummy {
        dummy clone() const {
            return *$self;
        }
    }
    

    Unfortunately, this does not end here, as our python code has to modified:

    import dummy_wrap
    d1 = dummy_wrap.dummy()
    d2 = d1.clone()
    d1.set_attr(12)
    print(d2.get_attr()) # now this prints '0'