3

Everything works in Visual Studio 2017, but I get linker errors in GCC (6.5.0).

Here is some sample code that isolates my problem:

#include <iostream>

struct Foo{
    static constexpr const char* s[] = {"one","two","three"};
};

int main(){
    std::cout << Foo::s[0] << std::endl;  //this works in both compilers
    const char* const* str_ptr = nullptr;
    str_ptr = Foo::s;                     //LINKER ERROR in GCC; works in VS
    std::cout << str_ptr[1] << std::endl; //works in VS
    return 0;
}

In GCC, I get undefined reference to 'Foo::s'. I need the initialization of Foo::s to stay in the declaration of the struct, which is why I used constexpr. Is there a way to reference Foo::s dynamically, i.e. with a pointer?

More Background Info

I'll now explain why I want to do this. I am developing embedded software to control configuration of a device. The software loads config files that contain the name of a parameter and its value. The set of parameters being configured is determined at compile-time, but needs to be modular so that it's easy to add new parameters and expand them as development of the device continues. In other words, their definition needs to be in one single place in the code base.

My actual code base is thousands of lines and works in Visual Studio, but here is a boiled-down toy example:

#include <iostream>
#include <string>
#include <vector>

//A struct for an arbitrary parameter
struct Parameter {
    std::string paramName;
    int max_value;
    int value;
    const char* const* str_ptr = nullptr;
};

//Structure of parameters - MUST BE DEFINED IN ONE PLACE
struct Param_FavoriteIceCream {
    static constexpr const char* n = "FavoriteIceCream";
    enum { vanilla, chocolate, strawberry, NUM_MAX };
    static constexpr const char* s[] = { "vanilla","chocolate","strawberry" };
};
struct Param_FavoriteFruit {
    static constexpr const char* n = "FavoriteFruit";
    enum { apple, banana, grape, mango, peach, NUM_MAX };
    static constexpr const char* s[] = { "apple","banana","grape","mango","peach" };
};

int main() {
    //Set of parameters - determined at compile-time
    std::vector<Parameter> params;
    params.resize(2);

    //Configure these parameters objects - determined at compile-time
    params[0].paramName = Param_FavoriteIceCream::n;
    params[0].max_value = Param_FavoriteIceCream::NUM_MAX;
    params[0].str_ptr = Param_FavoriteIceCream::s; //!!!! LINKER ERROR IN GCC !!!!!!

    params[1].paramName = Param_FavoriteFruit::n;
    params[1].max_value = Param_FavoriteFruit::NUM_MAX;
    params[1].str_ptr = Param_FavoriteFruit::s; //!!!! LINKER ERROR IN GCC !!!!!!

    //Set values by parsing files - determined at run-time
    std::string param_string = "FavoriteFruit"; //this would be loaded from a file
    std::string param_value = "grape"; //this would be loaded from a file
    for (size_t i = 0; i < params.size(); i++) {
        for (size_t j = 0; j < params[i].max_value; j++) {
            if (params[i].paramName == param_string
            && params[i].str_ptr[j] == param_value) {
                params[i].value = j;
                break;
            }
        }
    }
    return 0;
}

As you can see, there are enums and string arrays involved and these need to match, so for maintenance purposes I need to keep these in the same place. Furthermore, since this code is already written and will be used in both Windows and Linux environments, the smaller the fix, the better. I would prefer not to have to re-write thousands of lines just to get it to compile in Linux. Thanks!

Ben Hershey
  • 350
  • 3
  • 8
  • This might be related to how you define the static member, not just declare it. What happens if you add `inline` as in the following: `inline static constexpr const char* s[] = {"one","two","three"};`? – alter_igel Feb 14 '19 at 21:40
  • @alterigel Since C++17 `static constexpr` class member variables are implicitly `inline`. – HolyBlackCat Feb 14 '19 at 21:42
  • OP, does it work if you use `-std=c++17`? – HolyBlackCat Feb 14 '19 at 21:42
  • pretty sure this is a bug. gcc 7.1+ compiles fine using c++17. – NathanOliver Feb 14 '19 at 21:44
  • If you can't get C++17 support, a way to solve your problem would be to replace the static data members with static member functions (defined in the class definition) that return the desired constants. Functions defined in the class definition are implicitly inline (this has been true since C++98). – Brian Bi Feb 14 '19 at 22:48

3 Answers3

4

The program is valid in C++17. The program is not valid in C++14 or older standards. The default standard mode of GCC 6.5.0 is C++14.

To make the program conform to C++14, you must define the static member (in exactly one translation unit). Since C++17, the constexpr declaration is implicitly an inline variable definition and thus no separate definition is required.

Solution 1: Upgrade your compiler and use C++17 standard (or later if you're from the future), which has inline variables. Inline variables have been implemented since GCC 7.

Solution 2: Define the variable outside the class definition in exactly one translation unit (the initialisation remains in the declaration).

eerorika
  • 232,697
  • 12
  • 197
  • 326
3

It looks like you are not using C++17, and pre-C++17 this is undefined behavior, and one that I really dislike. You do not have a definition for your s, but you are ODR-using it, which means that you need to have a definition.

To define this, you have to define it in .cpp file.

In C++17 that would be the valid code. I am not familiar with MSVC, so I am not sure why it works fine there - be it that it is compiled as C++17, or because it is just a different manifestation of undefined behavior.

SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • 2
    It's well-formed since C++17, since the variable is [implicitly `inline`](https://en.cppreference.com/w/cpp/language/inline). (Not my downvote.) – HolyBlackCat Feb 14 '19 at 21:43
  • @HolyBlackCat It is clear that OP is not using C++17. Probably I should have added it to the answer as well. – SergeyA Feb 14 '19 at 21:46
  • From [cppreference](https://en.cppreference.com/w/cpp/language/static#Constant_static_members): "If a static data member of _LiteralType_ is declared `constexpr`, it must be initialized with an initializer in which every expression is a constant expression, right inside the class definition." (and an array of string literals satisfies _LiteralType_). Not my downvote either but I hope that helps – alter_igel Feb 14 '19 at 21:47
  • @alterigel I do not see how this quote contradicts my answer. – SergeyA Feb 14 '19 at 21:48
  • And for those curious about what ODR-using is, [What does it mean to “ODR-use” something?](https://stackoverflow.com/questions/19630570/what-does-it-mean-to-odr-use-something) – Richard Chambers Feb 14 '19 at 21:48
  • @SergeyA since `s` is a static constexpr member satisfying the LiteralType requirement, according to the quote, it must be defined inside the class definition – alter_igel Feb 14 '19 at 21:50
  • @alterigel no, according to the quote, it must be *initialized* inside the class definition. It says nothing about definition. (But the next paragraph does, and this answer conforms to that as well) – eerorika Feb 14 '19 at 22:52
  • @eerorika Ah yes, I was confused. Thank you for aiding my understanding. – alter_igel Feb 14 '19 at 23:07
  • Thanks guys. I quickly realized that the reason MSVC works is because it's C++17, and restructuring to define outside the class makes it work in GCC C++14. I can still keep the definition in the header file near the declaration, so I'm happy enough. – Ben Hershey Feb 18 '19 at 13:44
2

For C++98, C++11, and C++14 you need to explicitly tell the compiler where the initialization of Foo::s lives (see below) and you are good to go.

struct Foo{
    static const char* s[];
};
const char* Foo::s[] = {"one","two","three"};

And as explained in one of the comments your initialization is OK from C++17.

Bo R
  • 2,334
  • 1
  • 9
  • 17