In another question I learnt how to expose a function returning a C++ object to Python by copying the object. Having to perform a copy does not seem optimal. How can I return the object without copying it? i.e. how can I directly access the peaks returned by self.thisptr.getPeaks(data) in PyPeakDetection.getPeaks (defined in peak_detection_.pyx)?
peak_detection.hpp
#ifndef PEAKDETECTION_H
#define PEAKDETECTION_H
#include <string>
#include <map>
#include <vector>
#include "peak.hpp"
class PeakDetection
{
    public:
        PeakDetection(std::map<std::string, std::string> config);
        std::vector<Peak> getPeaks(std::vector<float> &data);
    private:
        float _threshold;               
};
#endif
peak_detection.cpp
#include <iostream>
#include <string>
#include "peak.hpp"
#include "peak_detection.hpp"
using namespace std;
PeakDetection::PeakDetection(map<string, string> config)
{   
    _threshold = stof(config["_threshold"]);
}
vector<Peak> PeakDetection::getPeaks(vector<float> &data){
    Peak peak1 = Peak(10,1);
    Peak peak2 = Peak(20,2);
    vector<Peak> test;
    test.push_back(peak1);
    test.push_back(peak2);
    return test;
}
peak.hpp
#ifndef PEAK_H
#define PEAK_H
class Peak {
    public:
        float freq;
        float mag;
        Peak() : freq(), mag() {}
        Peak(float f, float m) : freq(f), mag(m) {}
};
#endif
peak_detection_.pyx
# distutils: language = c++
# distutils: sources = peak_detection.cpp
from libcpp.vector cimport vector
from libcpp.map cimport map
from libcpp.string cimport string
cdef extern from "peak.hpp":
    cdef cppclass Peak:
        Peak()
        Peak(Peak &)
        float freq, mag
cdef class PyPeak:
    cdef Peak *thisptr
    def __cinit__(self):
        self.thisptr = new Peak()
    def __dealloc__(self):
        del self.thisptr
    cdef copy(self, Peak &other):
        del self.thisptr
        self.thisptr = new Peak(other)
    def __repr__(self):
        return "<Peak: freq={0}, mag={1}>".format(self.freq, self.mag)
    property freq:
        def __get__(self): return self.thisptr.freq
        def __set__(self, freq): self.thisptr.freq = freq
    property mag:
        def __get__(self): return self.thisptr.mag
        def __set__(self, mag): self.thisptr.mag = mag
cdef extern from "peak_detection.hpp":
    cdef cppclass PeakDetection:
        PeakDetection(map[string,string])
        vector[Peak] getPeaks(vector[float])
cdef class PyPeakDetection:
    cdef PeakDetection *thisptr
    def __cinit__(self, map[string,string] config):
        self.thisptr = new PeakDetection(config)
    def __dealloc__(self):
        del self.thisptr
    def getPeaks(self, data):
        cdef Peak peak
        cdef PyPeak new_peak
        cdef vector[Peak] peaks = self.thisptr.getPeaks(data)
        retval = []
        for peak in peaks:
            new_peak = PyPeak()
            new_peak.copy(peak) # how can I avoid that copy?
            retval.append(new_peak)
        return retval
                The Python return statement is a special statement that you can use inside a function or method to send the function's result back to the caller. A return statement consists of the return keyword followed by an optional return value. The return value of a Python function can be any Python object.
A function can also return objects either by value or by reference. When an object is returned by value from a function, a temporary object is created within the function, which holds the return value. This value is further assigned to another object in the calling function.
If you have a modern C++ compiler and can use rvalue references, move constructors and std::move it's pretty straight-forward. I think the easiest way is to create a Cython wrapper for the vector, and then use a move constructor to take hold of the contents of the vector.
All code shown goes in peak_detection_.pyx.
First wrap std::move. For simplicity I've just wrapped the one case we want (vector<Peak>) rather than messing about with templates.
cdef extern from "<utility>":
    vector[Peak]&& move(vector[Peak]&&) # just define for peak rather than anything else
Second, create a vector wrapper class. This defines the Python functions necessary to access it like a list. It also defines a function to call the move assignment operator
cdef class PyPeakVector:
    cdef vector[Peak] vec
    cdef move_from(self, vector[Peak]&& move_this):
        self.vec = move(move_this)
    def __getitem__(self,idx):
        return PyPeak2(self,idx)
    def __len__(self):
        return self.vec.size()
Then define the class the wraps the Peak. This is slightly different to your other class in that it doesn't own the Peak it wraps (the vector does). Otherwise, most of the functions remain the same
cdef class PyPeak2:
    cdef int idx
    cdef PyPeakVector vector # keep this alive, since it owns the peak rather that PyPeak2
    def __cinit__(self,PyPeakVector vec,idx):
        self.vector = vec
        self.idx = idx
    cdef Peak* getthisptr(self):
        # lookup the pointer each time - it isn't generally safe
        # to store pointers incase the vector is resized
        return &self.vector.vec[self.idx]
    # rest of functions as is
    # don't define a destructor since we don't own the Peak
Finally, implement getPeaks()
cdef class PyPeakDetection:
    # ...    
    def getPeaks(self, data):
        cdef Peak peak
        cdef PyPeak new_peak
        cdef vector[Peak] peaks = self.thisptr.getPeaks(data)
        retval = PyPeakVector()
        retval.move_from(move(peaks))
        return retval
Alternative approaches:
If Peak was nontrivial you could go for an approach where you call move on Peak rather that on the vector, as you construct your PyPeaks. For the case you have here move and copy will be equivalent for `Peak.
If you can't use C++11 features you'll need to change the interface a little. Instead of having your C++ getPeaks function return a vector have it take an empty vector reference (owned by PyPeakVector) as an input argument and write into it. Much of the rest of the wrapping remains the same.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With