问题
I want to force template instantiation.
The below code works (print 1
) at g++ ( http://coliru.stacked-crooked.com/a/33986d0e0d320ad4 ).
However, it prints wrong result (0
) at Visual C++ ( https://rextester.com/WGQG68063 ).
#include <iostream>
#include <string>
template <int& T>struct NonTypeParameter { };
//internal implementation
int lala=0;
template <typename T> struct Holder{
static int init;
};
template <typename T> int Holder<T>::init = lala++;
//tool for user
template <typename T> struct InitCRTP{
using dummy=NonTypeParameter<Holder<T>::init>;
};
class WantInit : public InitCRTP<WantInit>{};//user register easily
int main(){
std::cout << lala << std::endl;
}
Is it a Visual C++ compiler bug, or a kind of undefined behavior?
If it is Visual C++ bug, how to workaround it (while still keep it beautiful)?
Edit: Change class -> struct as Max Langhof (and many people) recommended. Thank.
Bounty Reason
With opposite solutions from StoryTeller and Maxim Egorushkin and their in-depth discussion (thank!), this sounds like a fuzzy area of C++ rule.
If it is Visual C++ bug, I wish the issue to be certain enough to report.
Moreover, I still wish for a nice workaround, because this technique is very shiny for custom type-id generation. Explicit instantiation is not so convenient.
Note: I awarded bounty to Kaenbyou Rin, because, for me, it is easy to understand.
It doesn't means that the rest of answers are less correct or less useful.
I am still not sure which is a correct one. Readers should proceed with caution.
For safety, I will assume that I just can't use the feature (for now). Thanks everyone.
回答1:
There is a compiler bug involved, for sure. We can verify it by changing InitCRTP
a bit:
template <typename T, typename = NonTypeParameter<Holder<T>::init>>
struct InitCRTP {
};
Now referring to any InitCRTP<T>
specialization must use Holder<T>::init
to determine the second template argument. This in turn should force an instantiation of Holder<T>::init
, and yet VS doesn't instantiate that.
In general, using the CRTP class as a base should have instantiated all the declarations inside the class, including that of dummy
. So that too should have worked.
We can verify it further. Declarations of member functions are instantiated along with the class, when used as a base:
template <typename T> struct InitCRTP{
using dummy=NonTypeParameter<Holder<T>::init>;
void dummy2(dummy);
};
Still, VC++ is stubborn. Given all of this, and the behavior exhibited by both Clang and GCC, this is a VC++ bug.
回答2:
class WantInit : public InitCRTP<WantInit>
does neither instantiate InitCRTP<WantInit>::dummy
, nor Holder<WantInit>::init
because they aren't referred to by something actually used in the program. The implicit instantiation chain in your code doesn't require instantiating Holder<T>::init
, see implicit instantiation:
This applies to the members of the class template: unless the member is used in the program, it is not instantiated, and does not require a definition.
A fix is to use explicit template instantiation:
template struct Holder<void>;
That causes Holder<void>
to be instantiated along with all its non-template members.
Alternatively, you can instantiate just Holder<T>::init
member, e.g.:
static_cast<void>(Holder<void>::init);
IMO, gcc and clang are overly eager to instantiate things that aren't referred to. Such a behaviour doesn't break or reject valid code, so that is hardly a bug, but depending on such a specific behaviour for side effects is brittle and non-portable.
回答3:
Let's try to definitely ODR-use the init
member.
#include <iostream>
#include <string>
int lala=0;
template <typename T> struct Holder{
static int init;
};
template <typename T> int Holder<T>::init = lala++;
template <typename T> struct InitCRTP{
InitCRTP() { (void)Holder<T>::init; }
};
class WantInit : public InitCRTP<WantInit>{};
int main(){
std::cout << lala << std::endl;
// WantInit w; <---------------------------- look here
}
Now the outcome of the program changes if the commented-out line is uncommented. IMHO template instantiation status or ODR-use status of anything cannot possibly depend on whether or not some non-template function (the WantInit constructor in this case) is called. I would say there is a rather strong smell of a bug.
回答4:
I believe @MaximEgorushkin is right about the fact that dummy
is not really instantiated.
dummy
is declared (because it is a type alias declaration), and in order to declare this alias, NonTypeParameter<Holder<T>::init>
is declared. In order to declare NonTypeParameter<Holder<T>::init>
, its template parameter Holder<T>::init
has to be declared, thus Holder<T>
is also declared.
The standard demands that when a template class is instantiated, its deleted member functions are defined. [temp.spec]
The implicit instantiation of a class template specialization causes: [...]
—— The implicit instantiation of the definitions of deleted member functions, unscoped member enumerations, and member anonymous unions.
And a reference to void
should result in a compile error.
We can use this to test whether a specific template is specialized or not.
#include <iostream>
#include <string>
template <int& T, typename U> struct NonTypeParameter {
U& f() = delete;
};
//internal implementation
int lala = 0;
template <typename T> struct Holder {
T& f() = delete;
static int init;
};
template <typename T> int Holder<T>::init = lala++;
//tool for user
template <typename T> struct InitCRTP {
using dummy = NonTypeParameter<Holder<T>::init, void>;
};
class WantInit : public InitCRTP<WantInit> {};//user register easily
int main() {
std::cout << lala << std::endl;
}
This code would compile because NonTypeParameter<Holder<T>::init, void>
is only declared, not instantiated.
But if we change class WantInit : public InitCRTP<WantInit>
into
class WantInit : public InitCRTP<void>
Fails to compile in MSVC, g++ and clang.
This is because the declaration of NonTypeParameter<Holder<void>::init, void>
needs the implicit instantiation of Holder<void>
.
The problem OP has encountered is solely due to MSVC's ignorance of Holder<T>::init
being ODR-used:
#include <iostream>
template <int& T> struct NonTypeParameter { };
int lala = 0;
template <typename T> struct Holder {
static int init;
};
template <typename T> int Holder<T>::init = lala++;
int main() {
NonTypeParameter<Holder<int>::init> odr;
std::cout << lala << std::endl;
}
MSVC would output 0
. This means it doesn't realize that Holder<int>::init
has been ODR-used.
Compiler Explorer Link
来源:https://stackoverflow.com/questions/57953599/force-template-instantiation-via-typedef-success-at-g-fail-at-visual-c