9

I have a function that scans the user's file system, fills a vector with the paths, then either sorts it or not. Since the user should be able to decide at compile-time whether he wants the vector sorted or not, I use templates and helper classes in place of a much desired (but not existing) "static if".

Consider this code:

enum class Sort{Alphabetic, Unsorted};  

template<Sort TS> struct SortHelper;
template<> struct SortHelper<Sort::Alphabetic>
{
    static void sort(vector<string>& mTarget) { sort(begin(mTarget), end(mTarget)); }
};
template<> struct SortHelper<Sort::Unsorted>
{
    static void sort(vector<string>&) { }
};

template<Sort TS> struct DoSomethingHelper
{
    static void(vector<string>& mTarget)
    {
         // do something with mTarget
         SortHelper<TS>::sort(mTarget);
    }   
};

The code I've written above is GREATLY simplified from the original, which takes multiple template parameters to allow the user to customize even further the results of the function at compile-time.

Is there an alternative to using all of these helper classes? It gets really messy and hard to read.

Ideally, this is what I would like to write:

enum class Sort{Alphabetic, Unsorted};  
template<Sort TS> struct DoSomethingHelper
{
    static void(vector<string>& mTarget)
    {
         // do something with mTarget
         static_if(TS == Sort::Unsorted) { /* do nothing */ }
         static_if(TS == Sort::Alphabetic) { sort(begin(mTarget), end(mTarget)); }
    }   
};
Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • 1
    Not existing, and [not likely to exist](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3613.pdf). – Morwenn Jun 10 '13 at 13:17
  • 3
    Um, why do you care about doing this at compile time? It sounds like you're overthinking this a lot. If you're first scanning a directory tree, a single run-time branch will be far below benchmarking noise in performance impact. – Sebastian Redl Jun 10 '13 at 13:19
  • 2
    If all branches are syntactically and semantically valid, you can use a normal `if` and the optimiser will eliminate the inactive branches, as well as unneeded comditionals/jumps. If the inactive branches don't compile, though, helpers are about the only way. – Angew is no longer proud of SO Jun 10 '13 at 13:19
  • @SebastianRedl: I agree that the performance impact could be minimal, but it really seems "wrong" to me that something that could be easily determined at compile-time has to be determined at run-time. – Vittorio Romeo Jun 10 '13 at 13:22
  • Maybe my Answer (http://stackoverflow.com/a/16811829/1918154) to an simular question will help. – Jan Herrmann Jun 10 '13 at 13:23
  • @Angew: Sounds great! Is there more detailed information about this kind of optimization? (examples, things to take note of, etc...) – Vittorio Romeo Jun 10 '13 at 13:23
  • @Morwenn: Not related to the original question, but the paper left me perplexed - the main argument against static if is that "it could be used for evil things"... but, isn't this true for most C++ features? From macros, to unsafe memory management, if the programmer wants to write bad code he is able to. – Vittorio Romeo Jun 10 '13 at 13:26
  • @Vee I want *less* of those features, not more. – Luc Danton Jun 10 '13 at 13:28
  • 1
    @Vee The features you cited actually kind of come from C and are worked around whenever possible. `static if` if easy to use and understand, but most of the arguments against it are indeed real problems. – Morwenn Jun 10 '13 at 13:30
  • 3
    @Vee it isn't that "it could be used for evil things" as much "its use will make good things impossible". They want those good things, and they have alternative solutions that are not "static_if" which solve many of the practical problems that "static_if" solves... – Yakk - Adam Nevraumont Jun 10 '13 at 13:30
  • Well, a few years later and `if constexpr` (formerly known as `static if`) may actually very well be part of C++17. – P-Gn May 20 '16 at 12:16

3 Answers3

18

Since your value is known at compile time (non-template type parameter) you can perfectly write a "normal" if:

template<Sort TS>
void someFunction(vector<string>& mTarget)
{
     if (TS == Sort::Alphabetic) { sort(begin(mTarget), end(mTarget)); }
     // else if (TS == Sort::Unsorted) {}
}

The compiler will perform constant folding and dead code elimination (if those optimisations are enabled, of course), and the result will be exactly the same as if you used the hypothetical static_if.

syam
  • 14,701
  • 3
  • 41
  • 65
  • 1
    Looks fantastic - is there any detailed documentation about this kind of optimization? I would love to know when it's 100% guaranteed, or when the compiler has trouble doing it, etc... – Vittorio Romeo Jun 10 '13 at 13:24
  • gcc has [this page](http://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html) that explains every single optimization option you can control, include -fdce, dead code elimination – SirGuy Jun 10 '13 at 13:26
  • 2
    It is not at all guaranteed by the standard, but it is allowed (by the as-if clause: if you cannot reach dead code, it is the same as-if it is not there), but at any non-trivial optimization level, I have never ever seen a single C++ compiler *not* do it. For portability, make it a simple comparison with a compile-time obvious value. Note that the code in the branch must be legal to compile if or if not the condition is true. – Yakk - Adam Nevraumont Jun 10 '13 at 13:28
  • @Vee: there's little to know. If your conditional expression can be evaluated at compile-time (eg. you could use it in a `static_assert`) and the relevant compiler options are enabled (they always are in release mode) then it works. – syam Jun 10 '13 at 13:29
  • 1
    @Yakk: "*It is not at all guaranteed by the standard*" => good point, this had to be mentioned. – syam Jun 10 '13 at 13:31
  • 1
    Note that some compilers (VC++) will warn about "conditional expression is constant", which can be annoying to `#pragma` away if you need to compile at /W4 and warnings-as-errors – TemplateRex Jun 10 '13 at 13:52
  • @TemplateRex: do we really need to care about subpar warnings ? gcc has some warnings too that should be flow-dependent or instantiation-dependent but are not, etc... and thus generate a lot of noise. Personally, I'd rather turn them off rather than pollute my code. – Matthieu M. Jun 10 '13 at 14:17
  • @MatthieuM. I turn this warning off as well, but it can be annoying to maintain a compiler-dependent list of invalid warnings – TemplateRex Jun 10 '13 at 14:25
  • 2
    I've decided to "trust the compiler", and refactored my existing code using simple if/else if directives. https://github.com/SuperV1234/SSVUtils/commit/906b4329120e6191e6ec10ee057f7f879320657b – Vittorio Romeo Jun 10 '13 at 15:02
12

I am afraid there has been a misunderstanding about the usage of static_if.

Certainly you can use static_if (or whatever trick you wish really) to try and get some optimization, but that is not its first goal.

The first goal of static_if is semantical. Let me demonstrate this with std::advance. A typical implementation of std::advance will use a type switch to choose, at compile time, between an O(1) implementation (for Random Access Iterators) and an O(n) implementation (for the others):

template <typename It, typename D>
void advance_impl(It& it, D d, random_access_iterator_tag)
{
    it += d;
}

template <typename It, typename D>
void advance_impl(It& it, D d, bidirectional_iterator_tag)
{
    if (d > D(0)) { for (D i(0); i < d; ++i) { ++it; } }
    else          { for (D i(0); i > d; --i) { --it; } }
}

template <typename It, typename D>
void advance_impl(It& it, D d, input_iterator_tag)
{
    for (D i(0); i < d; ++i) { ++it; }
}

And finally:

template <typename It, typename D>
void advance(It& it, D d)
{
    typename std::iterator_traits<It>::iterator_category c;
    advance_impl(it, d, c);
}

Why not use just a if in this case ? Because it would not compile.

  • a Bidirectional Iterator does not support +=
  • an Input Iterator (or Forward Iterator) does not support --

Thus, the only way to implement the functionality is to statically dispatch to a function only using the available operations on the given type.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
0

What about template specialization?

#include <vector>
#include <iostream>
#include <algorithm>

using namespace std;

enum class Sort {
    Alphabetic, 
    Unsorted
};

template<Sort TS> struct DoSomethingHelper {
    static void someFunction(vector<string>& mTarget)
    {}  
};

template<> struct DoSomethingHelper<Sort::Unsorted> {
    static void someFunction(vector<string>& mTarget) {

    }  
};

template<> struct DoSomethingHelper<Sort::Alphabetic> {
    static void someFunction(vector<string>& mTarget) {
        sort(begin(mTarget), end(mTarget));
    }  
};

int main() {
    vector<string> v = {{"foo", "bar", "foo2", "superman", ".."}};

    DoSomethingHelper<Sort::Alphabetic> helper;
    helper.someFunction(v);

    for (string& s : v) {
        cout << s << endl;
    }
    return 0;
}

Edit: I'm a idiot.

Tim
  • 5,521
  • 8
  • 36
  • 69