1

Consider a library that defines in a header file

struct Proj {
    struct Depth {
        static constexpr unsigned Width = 10u;
        static constexpr unsigned Height = 10u;
    };
    struct Video {
        static constexpr unsigned Width = 10u;
        static constexpr unsigned Height = 10u;
    };
};

The library gets compiled, and I'm now developing an application that links against this library. I thought for a long time it was some kind of visibility problem, but even after adding B_EXPORT (standard visibility stuff from CMake) everywhere with no change, I finally found the problem.

template <class Projection>
struct SomeApplication {
    SomeApplication() {
        unsigned height = std::max(// or std::max<unsigned>
            Projection::Depth::Height,
            Projection::Video::Height
         );
    }
};

I can't even reproduce the problem with a small dummy library / sample application. But using std::max in the application causes the link error, whereas if I just do it myself

unsigned height = Projection::Depth::Height;
if (height < Projection::Video::Height)
    height = Projection::Video::Height;

Everything works out. AKA there don't appear to be any specific issues with the visibility in terms of just using Projection::XXX.

Any thoughts on what could possibly cause this? This is on OSX, so this doesn't even apply.

svenevs
  • 833
  • 9
  • 24

1 Answers1

1

The problem is that Width and Height are declared, not defined in your structs. Effectively, this means there is no storage allocated for them.

Now recall the signature for std::max:

template<typename T>
const T& max(const T&, const T&);

Note the references: this means the addresses of the arguments are to be taken. But, since Width and Height are only declared, they don't have any storage! Hence the linker error.

Now let's consider the rest of your question.

Your hand-written max works because you never take any pointers or references to the variables.

You might be unable to reproduce this on a toy example because, depending on the optimization level in particular, a sufficiently smart compiler might evaluate max at compile time and save the trouble of taking the addresses at runtime. For instance, compare the disasm for no optimization vs -O2 for gcc 7.2: the evaluation is indeed done at compile-time!

As for the fix, it depends. You have several options, to name a few:

  1. Use constexpr getter functions instead of variables. In this case the values they return will behave more like temporary objects, allowing the addresses to be taken (and the compiler will surely optimize that away). This is the approach I'd suggest in general.
  2. Use namespaces instead of structs. In this case the variables will also be defined in addition to being declared. The caveat is that you might get duplicate symbol errors if they are used in more than one translation unit. The fix for that is only in form of C++17 inline variables.
  3. ...speaking of which, C++17 also changes the rules for constexpr static member variables (they become inline by default), so you won't get this error if you just switch to this standard.
0xd34df00d
  • 1,496
  • 1
  • 8
  • 17
  • Woah! That makes a lot of sense with such a clear, well thought out answer -- thanks for explaining it so carefully!!! I will have to go with `1`, the dependencies of my project require C++11, as well as this `Proj` class is used as one of many other possible projections. Keeping them self contained allows for the rest of the framework to be defined by the `struct`. I'm looking forward to moving on to C++17, and this is (yet another) reason to do so <3 – svenevs Dec 16 '17 at 04:05