6
\$\begingroup\$

I'm getting started on template meta-programming and I wrote a template to compute compile-time positive and negative float powers of 2. I would like some feedback on readability and overall code complexity as I feel like I might be defining too many structures for such a simple computation. The code has to be C++98 compatible.

// Sign of an integer
template<bool b> struct SIGN {static const int value = ( b * 2 ) - 1; };

// Positive powers of two (fast exponentiation)
template<unsigned int N> struct POW2_POS { 
    static const float value =
      POW2_POS<N%2>::value 
    * POW2_POS<N/2>::value 
    * POW2_POS<N/2>::value; 
};
template<> struct POW2_POS <1> { static const float value = 2.0f; };
template<> struct POW2_POS <0> { static const float value = 1.0f; };

// Negative powers of two
template<unsigned int N> struct POW2_NEG {static const float value = 1.0f / POW2_POS<N>::value; };

// Calls POW2_POS or POW2_NEG depending on boolean parameter
template<bool b, unsigned int N> struct POW2_SIGNED;
template<unsigned int N> struct POW2_SIGNED<true, N> { static const float value = POW2_POS<N>::value;};
template<unsigned int N> struct POW2_SIGNED<false, N> { static const float value = POW2_NEG<N>::value;};

// Integer power of two
template<int N> struct POW2 { 
    static const float value = POW2_SIGNED<(N>=0), SIGN<(N>=0)>::value * N>::value; 
};

Example program

#include <iostream>
int main()
{
    std::cout<<POW2<0>::value <<"\n";
    std::cout<<POW2<1>::value <<"\n";
    std::cout<<POW2<4>::value <<"\n";
    std::cout<<POW2<7>::value <<"\n";
    std::cout<<POW2<-4>::value <<"\n";
}

Output

1
2
16
128
0.0625
\$\endgroup\$
5
  • 2
    \$\begingroup\$ Prefer to use double instead of float. Because these are compile time constants down casting to float will cost you nothing but up casting to double from float will give you less precision than you would have had had you used doubles in your template. \$\endgroup\$
    – Emily L.
    May 25 '18 at 0:19
  • 1
    \$\begingroup\$ Also, all capital letters in identifies are usually reserved for macros. I would advise against using it for other identifiers. \$\endgroup\$
    – Emily L.
    May 25 '18 at 0:21
  • 2
    \$\begingroup\$ Why do you want to “get started” with the stuff we are finally glad to be rid of? \$\endgroup\$
    – JDługosz
    May 25 '18 at 9:12
  • \$\begingroup\$ Because unfortunately I'm forced to work on a very old compiler. \$\endgroup\$
    – Rch
    May 25 '18 at 10:46
  • \$\begingroup\$ @TobySpeight sure, I wrote one up. It was in the middle of the night and I just wanted to leave quick comments on my phone before shuteye. \$\endgroup\$
    – Emily L.
    May 25 '18 at 13:26
4
\$\begingroup\$

Your code is not C++98 compliant

Standards prior to C++11 only allows const integer static member initialisation (see here).

So unfortunately your code is not C++98 compliant and in fact does not compile. So technically your code is off-topic on this site. As it is not working as intended. Your code probably only compiles because you're using a compiler that has an extension to allow non integer const static members.

To get around this typically you would define a static method that returns the value. In fact, this is why std::numeric_limits<T>::max() is a function instead of a static member value. However these cannot be evaluated in a constant expression context. You need C++11 constexpr for that afaik.

General advice

Some general advice. Template parameters are compile time constants by definition. Any sensible compiler should do any necessary casts during compile time so you will not get a run-time penalty for using higher precision.

For this reason I advice that you use the largest types possible in your template to get the most precision at no additional cost. I.e. change float to double and int to long long.

You should consider wrapping the templates that are not intended for use by the user in a suitable namespace. Typically I use a nested namespace called detail.

Also identifiers in ALL_CAPS_WITH_UNDERSCORES are typically reserved for macros. So I advice against using this naming convention for your identifiers as it will confuse many readers.

Better implementation

If we restrict ourselves to the bits of the code that are C++98 compliant, then we can have a much easier implementation for the integer exponentiation.

template<unsigned long long exponent>
struct ipow_2{
    static const unsigned long long value = (1 << exponent);
};

If you want a C++98 compliant negative exponent you can use:

template<long long exponent>
struct pow_2{
    static double value(){
        if (exponent == 0) {
            return 1;
        } 
        else if (exponent > 0) {
            double half = pow_2<exponent/2>::value();
            return (1<<(exponent&1))*half*half;
        }
        return 1 / pow_2<-exponent>::value();
    };
};

But be aware that the same caveat for non-integer const initialisation still applies so it's not hugely useful in TMP for example.

If we accept use C++11 you can simplify and generalize to:

// Tail call optimization will transform this into the equivalent 
// loop
constexpr int64_t ipow_p(int64_t base, int exp, int64_t ans = 1) {
    return exp < 1 ?
        ans : ipow_p(base*base, exp/2, (exp % 2) ? ans*base : ans);
}

constexpr double ipow(int base, int exp){
    return exp > 0 ? ipow_p(base, exp) : 1.0 / ipow_p(base, -exp);
}

This is usable in a constant (TMP) context thanks to the constexpr qualifier and At -O2 the call will be expanded to a constant.

\$\endgroup\$
5
  • \$\begingroup\$ Thank you, this explains a lot. I was very surprised to see that this code didn't compile on c++11, I assume it's because it didn't have the same extension. I'll have to figure out what exactly is allowed on the compiler I work with. \$\endgroup\$
    – Rch
    May 25 '18 at 16:20
  • \$\begingroup\$ Regarding your solution, I have an issue with the fact that it doesn't allow for exponents higher than the size of a long (which should be shorter than the maximum exponent of a double or even a float). \$\endgroup\$
    – Rch
    May 25 '18 at 16:23
  • \$\begingroup\$ @Rch "...didn't compile on C++11" The standard is irrelevant here, your code is not valid, standard C++ in any version of the language. I used clang to compile which doesn't have the extensions that GCC has. GCC should show the same behavior if you pass -Wall -Wextra -pedantic -Werror... well, at least recent versions. \$\endgroup\$
    – Emily L.
    May 25 '18 at 18:32
  • \$\begingroup\$ @Rch The largest number a double can handle is 1.79769e+308, which corresponds to roughly 2^1024. Usinglong long as an exponent is more than enough to exceed DOUBLE_MAX as it is least 64 bits. The resulting value is however limited to 18446744073709551615 (and will overflow a few times over). If you want to use the same type of implementation as you had (without using member functions) unfortunately that is the largest you will be able to get until you can use C++11 and constexpr. The C++98 compliant member method version will cover your expected range with the mentioned caveat. \$\endgroup\$
    – Emily L.
    May 25 '18 at 18:34
  • \$\begingroup\$ Yes, that's what I meant. That bitshift acts as an exponent for only as long as the size of your integer allows it, so if your unsigned long long is 64 bits you can only go up to 2^63 with this, after which it should return 0. \$\endgroup\$
    – Rch
    May 25 '18 at 19:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.