Reputation: 1211
I believe the source of this is actually some C++ craziness, specifically with std::optional
, but the context came from OpenCV. OpenCV has a datastructure, cv::Mat
, and some variants (for gpu, etc). They made a standard wrapper around it, called cv::InputOutputArray
, for rw-able array-like things. typedef const _InputOutputArray& cv::InputOutputArray
, and _InputOutputArray is internally a void* obj and some flags indicating what kind of object it is. All standard output functions (for drawing) require this image type. Now, for a utility class, that does some preprocessing, I take this, as is, trivially copiable, and put it in a class member, and do processing on destruction. Everything worked correctly-enough, until I made one of the members std::optional
; then everything became bad.
// Working version
#include "opencv2/opencv.hpp"
#include <iostream>
#include <optional>
struct MyClass1 {
MyClass1(cv::InputOutputArray img, cv::Point origin) : _img(img), _origin(origin) {}
~MyClass1() {
std::cout << "Destroying MyClass1: img ok? " << !_img.empty() << std::endl;
if(!_img.empty()) {
cv::circle(_img, _origin, _radius, cv::Scalar(0, 0, 255), 3);
}
}
cv::InputOutputArray _img;
const cv::Point _origin;
int _radius = 10;
};
static inline MyClass1 myClass1(cv::InputOutputArray img, cv::Point origin) {
return MyClass1(img, origin);
}
#define PLINE() std::cout << __LINE__ << " "
int main(){
std::cout << "GNU C++ version: " << __GNUG__ << "." << __GNUC_MINOR__ << "." << __GNUC_PATCHLEVEL__ << std::endl;
std::cout << "OpenCV version: " << CV_VERSION << std::endl;
cv::Mat img(300, 300, CV_8UC3, cv::Scalar(255, 255, 255));
cv::Point origin(150, 150);
PLINE(); myClass1(img, origin); // OK; but no storing as variable
// PLINE(); { auto fmt = myClass1(img, origin); fmt._radius = __LINE__; } // SegFault: unknown operation 'empty'
// { auto& fmt = myClass1(img, origin); fmt._radius = __LINE__; } // Not Allowed, ofc: ref to temporary
PLINE(); { auto&& fmt = myClass1(img, origin); fmt._radius = __LINE__; } // OK, note && within sub-block
PLINE(); { auto&& fmt = myClass1(cv::InputOutputArray(img), origin); fmt._radius = __LINE__; } // OK...
auto ioa = cv::InputOutputArray(img);
PLINE(); { auto fmt = myClass1(ioa, origin); fmt._radius = __LINE__; } // OK, but cumbersome
//PLINE(); do { auto fmt = myClass1(img, origin); fmt._radius = __LINE__; } while(0); // SegFault: unknown operation 'empty'
PLINE(); do { auto&& fmt = myClass1(img, origin); fmt._radius = __LINE__; } while(0); // OK, same as above
cv::imwrite("build/mre_t1.png", img);
return 0;
}
Returns:
g++ -std=c++17 -Wall -Wextra -pthread -g -Og -ggdb mre.cpp -o build/mre -I/usr/local/include -L/usr/local/lib `pkg-config --cflags --libs opencv4` -lopencv_core -lopencv_imgproc -lopencv_highgui
GNU C++ version: 9.4.0
OpenCV version: 4.5.4
30 Destroying MyClass1: img ok? 1
33 Destroying MyClass1: img ok? 1
34 Destroying MyClass1: img ok? 1
36 Destroying MyClass1: img ok? 1
38 Destroying MyClass1: img ok? 1
But this, with std::optional
as a member, breaks!
#include "opencv2/opencv.hpp"
#include <iostream>
#include <optional>
struct MyClass2 {
MyClass2(cv::InputOutputArray img, cv::Point origin) : _img(img), _origin(origin) {}
~MyClass2() {
std::cout << "Destroying MyClass2: img ok? " << !_img.empty() << std::endl;
if(!_img.empty()) {
const int _radius = _radius_opt.value_or(10); // CHANGE
cv::circle(_img, _origin, _radius, cv::Scalar(0, 0, 255), 3);
}
}
cv::InputOutputArray _img;
const cv::Point _origin;
std::optional<int> _radius_opt = std::nullopt; // CHANGE
};
static inline MyClass2 myClass2(cv::InputOutputArray img, cv::Point origin) {
return MyClass2(img, origin);
}
#define PLINE() std::cout << __LINE__ << " "
int main(){
std::cout << "GNU C++ version: " << __GNUG__ << "." << __GNUC_MINOR__ << "." << __GNUC_PATCHLEVEL__ << std::endl;
std::cout << "OpenCV version: " << CV_VERSION << std::endl;
cv::Mat img(300, 300, CV_8UC3, cv::Scalar(255, 255, 255));
cv::Point origin(150, 150);
// Still working:
PLINE(); myClass2(img, origin);
auto ioa = cv::InputOutputArray(img);
PLINE(); { auto fmt = myClass2(ioa, origin); fmt._radius_opt = __LINE__; } // OK, but cumbersome
// BROKEN:
PLINE(); { auto&& fmt = myClass2(img, origin); fmt._radius_opt = __LINE__; } // && within sub-block
PLINE(); { auto&& fmt = myClass2(cv::InputOutputArray(img), origin); fmt._radius_opt = __LINE__; } // Uh...
PLINE(); do { auto&& fmt = myClass2(img, origin); fmt._radius_opt = __LINE__; } while(0); // same as above
cv::imwrite("build/mre_t2.png", img);
return 0;
}
With the last 3 failing:
33 Destroying MyClass2: img ok? 1
35 Destroying MyClass2: img ok? 1
37 Destroying MyClass2: img ok? 0
38 Destroying MyClass2: img ok? 0
39 Destroying MyClass2: img ok? 0
This has to be some sort of weird interaction between ownership and temporiness... but I don't understand where it comes from; optional allocations?
Upvotes: 0
Views: 42