Wednesday, January 5, 2022

[SOLVED] Different compilation + linking errors for static and constexpr between clang and gcc

Issue

I have the following code:

// template_header.hpp
#ifndef TEMPLATE_HEADER_HPP
#define TEMPLATE_HEADER_HPP

namespace template_header
{
    template <int dim1>
    /*static*/ constexpr int dim2 = 0;

    template <>
    /*static*/ constexpr int dim2<2> = 3;
    template <>
    /*static*/ constexpr int dim2<3> = 5;
}

#endif
// lib1.cpp
#include <array>
#include "template_header.hpp"

template <int dim1>
class lib1_class
{
public:
    std::array< double, template_header::dim2<dim1> > ar1 = {0};
};
// lib2.cpp
#include <array>
#include "template_header.hpp"

template <int dim1>
class lib1_class
{
public:
    std::array< double, template_header::dim2<dim1> > ar1 = {0};
};

If I compile any of the .cpp files with static uncommented, GCC gives me an "explicit template specialization cannot have a a storage class" error. If static is commented, I can compile both .cpp files and then link them together as a shared library with g++ lib1.o lib2.o -shared -o shared_lib.so.

However, if I compile with static commented out with clang, I get no problems during compilation, but I get a "multiple definition of template_header::dim2<2>'" error during linking. If I uncomment static`, then everything compiles and links fine.

I'm pretty confused about this, firstly given that this answer indicates that, since my constexpr's happen in a namespace scope, they ought to automatically be static and therefore should pose no problem for the linker even if static is commented out. Additionally, I don't understand why adding static beforehand would change how GCC compiles the .cpp files, given that it should be implicitly static. Any explanation of the errors + possible fixes are appreciated.

Edit: I am using C++14.


Solution

So, without inline variables, I was able to get something achieving your goals working. The basic idea is to have a "backend" struct to hold static members and then fully specialize that struct to your options. This method has the added benefit of causing a compiler error if you attempt to access a member of the backend using a template parameter that has not been defined yet.

The header file would look like

#ifndef TEMPLATED_HEADER_HPP
#define TEMPLATED_HEADER_HPP

namespace template_header {
  /**
   * the "primary" template of the backend struct
   *  notice I leave the variable we want undefined!
   *  if you prefer to have a default for all other values of dim1
   *  you would put that here
   */
  template<int dim1>
  struct backend {};

  template<>
  struct backend<2> {
    static constexpr int dim2 = 3;
  };

  template<>
  struct backend<3> {
    static constexpr int dim2 = 5;
  };

  /**
   * Helper constexpr
   *  this is optional, but makes code outside this file more readable
   *  also, I named it in a way for your source files to be unchanged.
   */
  template <int dim1>
  constexpr dim2 = backend<dim1>::dim2;
}

#endif

I have a working version of this idea on Compiler Explorer which verifies that both this works with both GCC and clang. If you were to try to call dim2<dim1> where dim1 does not equal 2 or 3, then a compiler error complaining about backend<dim1>::dims not defined will be generated. You could add other members to the backend specializations, taking care to keep the names the same across the different values of dim1.

Total side note, why are you setting ar1 = {0};? From my reading of the std array reference, this would only set the first element in the array to 0.



Answered By - Tom Eichlersmith