1

I have some c++ code that showed an error and I have not be able to understand why. I have however managed to reduce the code to a small example.

I am using MSVC 19.34.31944, and the last release (3.4.0) of the linear algebra library Eigen. The compilation options are /Oxt /std:c++20 /EHs plus the include of wherever Eigen is.

The first example:

#include "Eigen/Dense"
#include <iostream>

std::pair<Eigen::Vector2d, double> func() {
    return { Eigen::Vector2d{11, 0}, 0 };
};

int main(int argc, char *argv[]) {

    const Eigen::Vector2d &result = func().first;

    std::cout << *result.data() << "  " << *(result.data() + 1) << std::endl;
    std::cout << result.transpose() << std::endl;
    std::cout << func().first.transpose() << std::endl;

    return 0;
}

Here I should get a vector equal to [11, 0], so the output should be:

11  0
11  0
11  0

However, I have:

11  0
6.95158e-310 6.95145e-310
11  0

I cannot reproduce this behavior (meaning I get the correct one) in debug, or with g++ in Linux. Furthermore, if I add std::cout << result << std::endl (without .transpose()) at some point the bug disappear. All this leads me to think this is some memory error. At first I thought it was due to the fact that the result is a member of a rvalue that is saved as a constant reference but it seems to be valid. Note that getting the result not by reference gives the expected behavior, but since a seemingly useless print also hides the bug I cannot conclude this reference business is the cause.

Then I thought it is due to an incorrect usage of the transpose function. I then came to this second example:

#include "Eigen/Dense"
#include <iostream>
#include <numbers>

// Computes the angle between two 2D vectors in [-pi, pi]
inline double computeAngle(const Eigen::Vector2d &vec1, const Eigen::Vector2d &vec2) {
    double angle = std::atan2(vec2(1), vec2(0)) - std::atan2(vec1(1), vec1(0));
    angle = angle > std::numbers::pi ? angle - 2.0 * std::numbers::pi : angle;
    angle = angle < -std::numbers::pi ? angle + 2.0 * std::numbers::pi : angle;
    return angle;
}

std::pair<Eigen::Vector2d, double> func() {
    double coeffMid = std::sin(2 * std::abs(computeAngle(Eigen::Vector2d{ 5, -5 }, Eigen::Vector2d{ -5, -5 })));
    std::cout << coeffMid << std::endl;

    Eigen::Vector2d center = (Eigen::Vector2d{ 10, 0 } + Eigen::Vector2d{ 0, 0 }) / 2;

    return { Eigen::Vector2d{5, 0}, 0 };
}


int main(int argc, char *argv[]) {
    
    const Eigen::Vector2d &result = func().first;
    std::cout << &result << std::endl;

    for (size_t i = 0; i < 5; i++) {
        const Eigen::Vector2d foo = result + result;
        std::cout << &foo << " " << foo[0] << " " << foo[1] << std::endl;
    }
    return 0;
}

where I hope to get

1.22465e-16
0000001954B3F9B0
0000001954B3F990 10 0
0000001954B3F990 10 0
0000001954B3F990 10 0
0000001954B3F990 10 0
0000001954B3F990 10 0

but I have

1.22465e-16
00000025FB4FFEA0
00000025FB4FFEA0 10 0
00000025FB4FFEA0 20 0
00000025FB4FFEA0 40 0
00000025FB4FFEA0 80 0
00000025FB4FFEA0 160 0

This example does not make a lot of sense, because it was initially a larger one that was simplified. Some lines seems useless (in func, the ternaries in computeAngle) but they do change the behavior.

EDIT: I have slightly modified the example so that it shows the memory addresses of result and foo. It seems that the program uses the same address for both variable, which explains the wrong behavior. However, it seems to me that this (saving the member first of the output as a const reference) should be valid.

It seems to me I am missing something obvious, as both example are quite small, but I cannot figure what. I know it is not popular to capture a function output with a const reference, I have read and understand why, but still it is allowed by the standard so it is not the cause of this bug. Or else I did not understand the standard!

Does anybody have some insight on this?

Thanks in advance


At some point I thought this was more related to Eigen so I asked my question on their gitlab. Here is the link to the corresponding issue. They also seem to think it is related to a MSVC bug.

pierreXVI
  • 11
  • 3
  • 2
    If you turn the reference into a copy and the error goes away, then MSVC might simply not properly implement this very obscure rule. Note the first comment in the question you linked: "Tested with msvc Microsoft (R) C/C++ Optimizing Compiler Version 19.26.28805 for x64 on same date, and msvc incorrectly prints Destructor called, x = 10 even with /std:c++17" – Homer512 Aug 17 '23 at 14:24
  • 1
    This smells of an alignment issue. But per http://eigen.tuxfamily.org/dox-devel/group__TopicStructHavingEigenMembers.html and http://eigen.tuxfamily.org/dox-devel/group__TopicStlContainers.html, you should be ok given the versions of C++, Eigen, and compiler you're using. But just to double check, you might try it with something other than Vector2d which doesn't have the same alignment pitfalls: Eg, Vector3f, or VectorXd. Eigen also has a way to disable the alignment, see the bottom of the first link. – Nicu Stiurca Aug 17 '23 at 19:13
  • In case anyone is interested, @Homer512 is talking about a comment from [the question](https://stackoverflow.com/questions/35947296/about-binding-a-const-reference-to-a-sub-object-of-a-temporary) that was linked in a comment in the question I linked. I still have the wrong behavior in the second example if a replace `Eigen::Vector2d` with `Eigen::Matrix`, but it seems to work fine when using `Eigen::VectorXd`. However I don't know if this enough to conclude. – pierreXVI Aug 18 '23 at 09:32
  • 1
    The `VectorXd` could simply be a dangling pointer since it allocates dynamically while the others use the stack. You could construct a test case with a custom type and put printfs into the destructor and move constructor to see if and when objects are destroyed relative to their use. – Homer512 Aug 18 '23 at 11:12
  • @Homer512 Why would it be dangling? VectorXd cleans up after itself when it's destructed. Just because the original object's lifespan got extended beyond the function where it was created due to how references of temporaries work, doesn't mean the dtor doesn't eventually get called. – Nicu Stiurca Aug 18 '23 at 15:57
  • @pierreXVI If you're sure this is compiler specific, you might try playing with some relevant compiler flags. Eg, `/W4` to get all possible warnings, or `/Za` to disable compiler extensions. I'm not familiar wth msvc, so there are possibly other flags that might be relevant. – Nicu Stiurca Aug 18 '23 at 16:06
  • @NicuStiurca My assumption, based on the comment I cited above, is that MSVC destroys the `pair` even if you hold a reference to the contained member. So the pointer would be dangling in the sense that the `VectorXd` got destroyed but we still hold a reference to it. This would be easy to test with some `printf`s in destructors but I don't have an MSVC to run this on. – Homer512 Aug 18 '23 at 16:15
  • @Homer512 That would be an _extremely_ surprising compiler bug. – Nicu Stiurca Aug 23 '23 at 22:35
  • @NicuStiurca there are no warnings (apart from unused argc and argv), and the bug is still there with /Za. In the related Eigen issue (link is in the question) someone says that [as disabling inline function expansion fixes the bug then it probably comes from the compiler](https://gitlab.com/libeigen/eigen/-/issues/2709#note_1519991983). – pierreXVI Aug 24 '23 at 08:52
  • @Homer512 I cannot reproduce a similar bug using homemade classes with prints in the dtor, but it seems the issue you cited is fixed for me. – pierreXVI Aug 24 '23 at 08:53
  • I modified the question (see ***EDIT***) to see the addresses of the variables `result` and `foo`. It seems that the compiler decide to reuse the same memory address which leads to the apparent behavior. As I think this minimal code should be valid according to the standard, I now really think it is a compiler bug. – pierreXVI Aug 28 '23 at 12:36
  • Can you repro with a poor-man's version of a `Vector2d`? Per https://eigen.tuxfamily.org/dox/classEigen_1_1Matrix.html, a `Vector2d` boils down to `struct { double data[2]; };` plus the relevant ctor/getters/setters. I'm still not convinced there isn't some UB in how Eigen defines stuff. – Nicu Stiurca Aug 31 '23 at 22:01
  • @NicuStiurca I tried with something like `struct Vector { double data[2]; const Vector operator+(const Vector &rhs) const { return Vector{ data[0] + rhs.data[0], data[1] + rhs.data[1] }; } double operator [](const int i) const { return this->data[i]; } };` but I was not able to reproduce the bug. – pierreXVI Sep 01 '23 at 13:25
  • Have you opened a bug with MSVC yet? Either they or the Eigen folks need to account for why the poor-man's Vector works but the full-fat `Vector2d` doesn't. I still think it's likely an alignment issue, and there may or may not be UB involved in how Eigen achieves the alignment it needs. – Nicu Stiurca Sep 01 '23 at 15:54

0 Answers0