Strange behavior of C ++ class in Python with SWIG

I have a C ++ class with two methods (part of a larger project). The methods do a very similar job: the first normalizes the vector and returns it, and the second returns the normalized vector without changing the original.

vector3.h

:

class Vector3
{
public:
  Vector3(double a = 0.0, double b = 0.0, double c = 0.0) 
      : x(a), y(b), z(c) {}
  Vector3(const Vector3& other) : x(other.x), y(other.y), z(other.z) {}
  double length() const { return sqrt(x*x+y*y+z*z); }
  Vector3& normalize()
  {
    float_type n = length();
    n = float_type(1.0) / ( n ? n : 1.0 );
    this->x *= n;
    this->y *= n;
    this->z *= n;
    return *this;
  }
  Vector3 normalized() const
  {
    Vector3 v(*this);
    v.normalize();
    return v;
  }
  std::string toString()
  {
    std::ostringstream strm;
    strm << '(' << x << ", " << y << ", " << z << ')' ;
    return strm.str();
  }
  Vector3 operator+(const Vector3& other) const
  {
    return Vector3(x + other.x, y + other.y, z + other.z);
  }

private:
  double x,y,z;
};

      

I am building Python bindings for this class using SWIG (via cmake).

vector3.i

:

%include <stl.i>
%include <carrays.i>
%include <cpointer.i>
%module vectortest
%{
#include "vector3.h"
%}
%include "vector3.h"

      

CMakeLists.txt

:

cmake_minimum_required(VERSION 2.8)

add_executable("vtest" "vtest.cpp")

find_package(SWIG)
include(${SWIG_USE_FILE})

find_package(PythonLibs)
find_package(PythonInterp)

include_directories(${CMAKE_SOURCE_DIR})
include_directories(${PYTHON_INCLUDE_DIRS})

SET(CMAKE_SWIG_FLAGS "")
SET_SOURCE_FILES_PROPERTIES(SOURCE vector3.i PROPERTIES CPLUSPLUS ON)
SWIG_ADD_MODULE(vectortest_python python vector3.i )
SWIG_LINK_LIBRARIES(vectortest_python ${PYTHON_LIBRARIES} ${LIBS})
set_target_properties(_vectortest_python PROPERTIES PREFIX "_" OUTPUT_NAME "vectortest")

execute_process(COMMAND ${PYTHON_EXECUTABLE} -c "from distutils.sysconfig import get_python_lib; print (get_python_lib())" OUTPUT_VARIABLE PYTHON_SITE_PACKAGES OUTPUT_STRIP_TRAILING_WHITESPACE)

install(TARGETS ${SWIG_MODULE_vectortest_python_REAL_NAME} DESTINATION ${PYTHON_SITE_PACKAGES})
install(FILES ${CMAKE_BINARY_DIR}/vectortest.py DESTINATION ${PYTHON_SITE_PACKAGES})

      

The behavior in C ++ is fine.

vtest.cpp

:

#include <iostream>
#include "vector3.h"

int main()
{
  Vector3 v1(1,0,0);
  Vector3 v2(0,1,0);

  std::cout << (v1+v2).toString() << std::endl;
  std::cout << (v1+v2).normalized().toString() << std::endl;
  std::cout << (v1+v2).normalize().toString() << std::endl;

  return 0;
}

      

Output:

(1, 1, 0)
(0.707107, 0.707107, 0)
(0.707107, 0.707107, 0)

      

However, this behavior in Python is rather strange:

vtest.py

:

#!/usr/bin/python3

from vectortest import Vector3

v1 = Vector3(1,0,0)
v2 = Vector3(0,1,0)
print( (v1+v2).toString() )
print( (v1+v2).normalized().toString() )
print( (v1+v2).normalize().toString() )

      

outputs:

(1, 1, 0)
(0.707107, 0.707107, 0)
(0, 0.707107, 0)

      

The second method ( normalized()

) works as expected, however the first ( normalize()

) does not work. What is causing this? Any suggestions on how to fix this issue?

+3


source to share


1 answer


@donkopotamus and @JensMunk are right.
Your code is wrong in Python because the lifetime of temporary variables is different.
The lifetime of a C ++ temporary variable (v1 + v2) is the entire sequence of function calls in C ++. Python calls made on top of SWIG wrappers for each function call, that is, on individual Python objects. For Python:

(v1 + v2) - creates a temporary Python object (tp1 = v1 + v2), which lifetime is different from the sequence of calls.



(v1 + v2) .normalized () - returns another temporary Python object (tp2_1);
(v1 + v2) .normalize () - Returns another temporary Python object (tp2_2) that internally refers to the first temporary Python object (tp1).
The temporary Python object (tp1 = v1 + v2) decrements the reference count and can now be garbage collected, invalidating the contents of tp2_2.

(v1 + v2) .normalized (). toString () - gives a temporary Python object (tp3_1) made from a valid tp2_1, tp2_1 can now be garbage; (v1 + v2) .normalize (). toString () - gives a temporary Python object (tp3_2) from a possibly corrupted tp2_2
...

0


source







All Articles