2

I am working on a project where the behavior of a certain function need to switch between a few values:

class James{
public:
    James(){
        if(a==0){
            //do this
        }else{
            // do that
        }
    }
};

currently, 'a' is read from a config file at runtime. However, in practice, 'a' can be determined at compile-time, rather than run time. I am thinking about have a trait class

struct TraitZero{
    constexpr int a = 0;
};

struct TraitOne{
    constexpr int a = 1;
};

and then turn James into a template class

template<typename Trait>
class James{
    constexpr int a = Trait::a;
    public:
        James(){
            if(a=0){
                //do this
            }else{
                // do that
            }
        }
    };

I don't know where I got it wrong, but this does not compile.

I am wondering if anyone here has ever countered problems like this. Can anyone share some insights?

James Bond
  • 7,533
  • 19
  • 50
  • 64
  • 1
    `a=0` should be `a==0` – Barry Mar 03 '17 at 17:24
  • Did you mean a==0? – A. K. Mar 03 '17 at 17:25
  • 1
    Doesn't `constexpr int a` need to be `static` in your structs? Care to share the compile error? – qxz Mar 03 '17 at 17:25
  • 2
    You don't have to use a traits class btw. You can simply template `James` directly on an integer. It's fairly common to template things directly on booleans or enums particularly to control behavior. If you think you'll need much more stuff a traits class is fine but if you just want a compile time boolean or two, YAGNI. – Nir Friedman Mar 03 '17 at 20:46
  • yes, == rather than =, I am spoiled by CLion these days – James Bond Mar 03 '17 at 22:26
  • yes, you are right. Actually I do need quite a lot of these kinds of compile-time flags. – James Bond Mar 03 '17 at 22:27

3 Answers3

2

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_ifs 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.

Community
  • 1
  • 1
1

a data members must be declared as constexpr and static to be used the way you are trying to use them:

struct TraitZero{
    static constexpr int a = 0;
};

struct TraitOne{
    static constexpr int a = 1;
};

Put aside the fact that it's be ill-formed as it stands, you wouldn't be allowed to access it as Traits::a otherwise.
The same applies to the class James:

template<typename Trait>
class James{
    static constexpr int a = Trait::a;
    //...
};

Note also that probably the following isn't what you want:

if(a=0){

Even if you were allowed to modify a (and you are not for it's a static constexpr data member), in this case you would have assigned 0 to a and constantly got the else branch.
Most likely you were looking for something similar but slightly different:

if(a == 0){

Below is an example based on your code once fixed:

#include<iostream>

struct TraitZero{
    static constexpr int a = 0;
};

struct TraitOne{
    static constexpr int a = 1;
};

template<typename Trait>
class James{
static constexpr int a = Trait::a;
public:
    James(){
        if(a==0){
            std::cout << "0" << std::endl;
        }else{
            std::cout << "1" << std::endl;
        }
    }
};

int main() {
    James<TraitZero> j0;
    James<TraitOne> j1;
}
skypjack
  • 49,335
  • 19
  • 95
  • 187
-1

Why not pass a #define at compile time using the -D option? For example: Including a #define in all .c source files at compile time

Community
  • 1
  • 1
Tomer
  • 549
  • 4
  • 9