6

The following program compiles without warnings with -O0:

#include <iostream>

struct Foo
{
  int const& x_;
  inline operator bool() const { return true; }
  Foo(int const& x):x_{x} { }
  Foo(Foo const&) = delete;
  Foo& operator=(Foo const&) = delete;
};

int main()
{
  if (Foo const& foo = Foo(3))
    std::cout << foo.x_ << std::endl;

  return 0;
}

However with -O1 or higher it gives the warning:

maybe-uninitialized.cpp: In function ‘int main()’:
maybe-uninitialized.cpp:15:22: warning: ‘<anonymous>’ is used uninitialized in this function [-Wuninitialized]
 std::cout << foo.x_ << std::endl;

How do you get rid of this warning with -O1 and higher?

The motivation for this is for a CHECK(x) macro which must capture a const reference rather than a value so as not to trigger a destructor, copy constructor, etc. as well as print out a value.

Resolution is at the bottom

Edit:

$ g++ --version
g++ (GCC) 8.2.1 20181127

No warnings:  g++ maybe-uninitialized.cpp -Wall -O0
With warning: g++ maybe-uninitialized.cpp -Wall -O1

Edit 2 in response to @Brian

#include <iostream>

struct CheckEq
{
  int const& x_;
  int const& y_;
  bool const result_;
  inline operator bool() const { return !result_; }
  CheckEq(int const& x, int const &y):x_{x},y_{y},result_{x_ == y_} { }
  CheckEq(CheckEq const&) = delete;
  CheckEq& operator=(CheckEq const&) = delete;
};

#define CHECK_EQ(x, y) if (CheckEq const& check_eq = CheckEq(x,y)) \
  std::cout << #x << " != " << #y \
    << " (" << check_eq.x_ << " != " << check_eq.y_ << ") "

int main()
{
  CHECK_EQ(3,4) << '\n';

  return 0;
}

The above is more interesting in that there are no warnings, but different output depending on the -O0 or -O1:

g++ maybe-uninitialized.cpp -O0 ; ./a.out
Output: 3 != 4 (3 != 4) 

g++ maybe-uninitialized.cpp -O1 ; ./a.out
Output: 3 != 4 (0 != 0) 

Edit 3 - Accepted answer

Thanks to @RyanHaining.

#include <iostream>

struct CheckEq
{
  int const& x_;
  int const& y_;
  explicit operator bool() const { return !(x_ == y_); }
};

int f() {
  std::cout << "f() called." << std::endl;
  return 3;
}

int g() {
  std::cout << "g() called." << std::endl;
  return 4;
}

#define CHECK_EQ(x, y) if (CheckEq const& check_eq = CheckEq{(x),(y)}) \
  std::cout << #x << " != " << #y \
    << " (" << check_eq.x_ << " != " << check_eq.y_ << ") "

int main() {
  CHECK_EQ(f(),g()) << '\n';
}

Output:

f() called.
g() called.
f() != g() (3 != 4) 

Features:

  • Each parameter to CHECK_EQ is only checked once.
  • Output displays inline code comparison as well as values.
Matt
  • 20,108
  • 1
  • 57
  • 70
  • Cannot reproduce - please provide the full commandline you used. –  May 03 '19 at 20:11
  • Which compiler are you using? – JVApen May 03 '19 at 20:12
  • 1
    I'm surprised you didn't get a warning from the non-optimized build. `Foo(3)` is never right because class member references will not extend the life of a temporary. Can you show what you are actually trying to acomplish with your macro and we should be able to help with that. I'll try and see if there is a dupe target for this. – NathanOliver May 03 '19 at 20:12
  • You could replace your macro with `auto CHECK_EQ = [](auto&& x, auto&& y) -> decltype(auto) { if (x != y) return std::cout << x << " != " << y << " (" << x << " != " << y << ") "; else return (std::cout); };` and get the same results without any UB, copying, or moving – NathanOliver May 03 '19 at 20:45
  • @NathanOliver Nice idea I will try that. – Matt May 03 '19 at 20:47
  • @NathanOliver that is (somewhat surprisingly) not true for aggregates, see answer below – Ryan Haining May 03 '19 at 21:10
  • 1
    @RyanHaining Very nice. I was not aware aggregates behaved that way. TIL :) (+1 on the answer btw) – NathanOliver May 03 '19 at 21:14
  • 1
    I forget to say, having `inline` is unnecessary for a definition a member function in a class body, the `operator bool` here is implicitly `inline` – Ryan Haining May 03 '19 at 21:52

2 Answers2

4

The code has undefined behaviour. Calling Foo's constructor causes the materialization of the prvalue 3 as a temporary object, which is bound to the parameter x. But the lifetime of that temporary object ends when the constructor exits, leaving x_ as a dangling reference by the time foo.x_ is evaluated.

You need to give more details about how you want your CHECK macro to work before it's possible to suggest a way to implement it without doing what you're doing here.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • Has it though? I don't see Foo cause materialization of anything but aliasing. I'm tired.... – Ted Lyngmo May 03 '19 at 20:19
  • I think you are on to something. I added an example with `CHECK_EQ` and now different output is produced depending on the optimization level. If you are able to suggest a way so that the `-O0` behavior is achieved with the `-O3` flag I would appreciate it, but you have already addressed the original question so I'll accept your answer if there are no better ones. – Matt May 03 '19 at 20:35
3

While a class with a user-defined constructor cannot extend the lifetime of the temporary, an aggregate can. By converting to an aggregate I can make your approach work

#include <iostream>

struct CheckEq
{
  int const& x_;
  int const& y_;
  bool const result_;
  explicit operator bool() const { return !result_; }
};

// adding () here for macro safety
#define CHECK_EQ(x, y) if (CheckEq const& check_eq = CheckEq{(x),(y),((x)==(y))}) \
  std::cout << #x << " != " << #y \
    << " (" << check_eq.x_ << " != " << check_eq.y_ << ") "

int main() {
  CHECK_EQ(3,4) << '\n';
}

This produces the same output with and without -O3 for me

Ryan Haining
  • 35,360
  • 15
  • 114
  • 174
  • Made a slight change so that each parameter is evaluated only once. Thanks again for the great answer. – Matt May 03 '19 at 21:26
  • Fyi `CHECK_EQ(3,std::min(4,5))` gives the warning with `-Wall -O3` but `CHECK_EQ(3,std::min(4,5)+0)` does not. Presumably this is due to `std::min` returning a `int const&` to its own parameter which doesn't survive when passed to `CheckEq`. – Matt May 04 '19 at 18:15