As has already been mentioned by skypjack, only static
data members can be constexpr
, and you need to use ==
instead of =
in the conditional.
That said, since you want to determine a
at compile time, it may be beneficial to you to branch based on a
at compile time, as well. To do this, you can use SFINAE or (as of C++17) constexpr if.
Assuming the following three traits...
struct TraitZero{
static constexpr int a = 0;
};
struct TraitOne{
static constexpr int a = 1;
};
template<size_t N>
struct TraitN {
static constexpr int a = N;
};
We can do this as...
SFINAE:
template<typename Trait>
class James {
// Unnecessary, we can access Trait::a directly.
//static constexpr int a = Trait::a;
public:
template<bool AZero = Trait::a == 0>
James(std::enable_if_t<AZero, unsigned> = 0) {
std::cout << "Trait::a is 0.\n";
}
template<bool AOne = Trait::a == 1>
James(std::enable_if_t<AOne, int> = 0) {
std::cout << "Trait::a is 1.\n";
}
template<bool ANeither = (Trait::a != 0) && (Trait::a != 1)>
James(std::enable_if_t<ANeither, long> = 0) {
std::cout << "Trait::a is neither 0 nor 1.\n";
}
};
What this does is conditionally select one of the versions of James()
based on the value of Traits::a
, using dummy parameters to enable overloading; this is simpler for functions other than constructors and destructors, as enable_if
can be used on their return type.
Note the use of template parameters, instead of directly checking Trait::a
in the enable_if
s themselves. As SFINAE can only be performed with types and expressions in the immediate context of the functions, these are used to "drag it in", so to speak; I like to perform the logic while doing so, as it minimises the intrusiveness of the enable_if
.
constexpr if:
template<typename Trait>
class James {
// Unnecessary, we can access Trait::a directly.
//static constexpr int a = Trait::a;
public:
James() {
if constexpr (Trait::a == 0) {
std::cout << "Trait::a is 0.\n";
} else if constexpr (Trait::a == 1) {
std::cout << "Trait::a is 1.\n";
} else {
std::cout << "Trait::a is neither 0 nor 1.\n";
}
}
};
As can be seen here, constexpr if can be used to create cleaner, more natural code than SFINAE, with the advantage that it will still be evaluated at compile time instead of run time; unfortunately, it isn't yet supported by most compilers. [In this particular case, each version of James()
will also be one machine instruction shorter (when compiled with GCC 7.0), due to not using a dummy parameter to differentiate between overloads.]
More specifically, with constexpr if, statement-false is discarded if the condition is true
, and statement-true is discarded if it's false
; in effect, this basically means that the compiler sees the entire constexpr if statement as the branch which would be executed. In this case, for example, the compiler will generate one of the following three functions, based on the value of Trait::a
.
// If Trait::a == 0:
James() {
std::cout << "Trait::a is 0.\n";
}
// If Trait::a == 1:
James() {
std::cout << "Trait::a is 1.\n";
}
// If Trait::a == anything else:
James() {
std::cout << "Trait::a is neither 0 nor 1.\n";
}
In either case, with the following code...
int main() {
James<TraitZero> j0;
James<TraitOne> j1;
James<TraitN<2>> j2;
}
The following output is generated:
Trait::a is 0.
Trait::a is 1.
Trait::a is neither 0 nor 1.
Each type's constructor will be coded specifically to output the appropriate line, and none of the three constructors will actually contain any branching.
Note that I only marked member a
as unnecessary out of personal preference; since I can access Trait::a
directly, I prefer to do so, so I won't have to check what a
is if I ever forgoet. Feel free to use it if you want to, or if it's needed elsewhere.