6

I'm trying to convert month from __DATE__ in format Mmm to number in compile time. I need it for gcc and MS VC 2012.

my attempt:

template <char C0, char C1, char C2>
struct month{};

template<> struct month < 'J', 'a', 'n'> { static const unsigned id = 1; };
template<> struct month < 'S', 'e', 'p'> { static const unsigned id = 9; };

static const char c0 = __DATE__[0];
static const char c1 = __DATE__[1];
static const char c2 = __DATE__[2];

static const unsigned currId = month<c0, c1, c2>::id;      //this gives error
static const unsigned currId2 = month<'S', 'e', 'p'>::id;  //this is fine

gives in MS VC

error C2970: 'month' : template parameter 'C0' : 'c0' : an expression involving objects with internal linkage cannot be used as a non-type argument

but on ideone works fine.

Is there a way to make this work cross platform/compiler?

edit

I need a compile-time constant So answers in similar question doesn't really help. I need to for example subtract two dates (current and some date in code) and give compile time error when difference between those two is large enough.

Community
  • 1
  • 1
relaxxx
  • 7,566
  • 8
  • 37
  • 64
  • String literals don't make good template parameters – gd1 Sep 06 '15 at 14:08
  • 1
    Is this questions helpful? http://stackoverflow.com/questions/19760221/c-get-the-month-as-number-at-compile-time?rq=1 – CompuChip Sep 06 '15 at 14:15
  • 1
    unfortunately not, because I need compile time constant to use it in another template... – relaxxx Sep 06 '15 at 14:19
  • FYI this will compile and run in MSVC 2015. They have a community edition which is free: https://www.visualstudio.com/en-us/downloads/download-visual-studio-vs.aspx – NathanOliver Sep 06 '15 at 14:23

3 Answers3

6

First: are you sure you need this at compile-time? If run-time is acceptable it's easy: http://www.keil.com/support/docs/1102.htm


But moving away from what's sane, let's have some compile-time fun!

You're using templates here but you really don't need to. You can use a massive expression of doom instead:

static const char c0 = __DATE__[0];
static const char c1 = __DATE__[1];
static const char c2 = __DATE__[2];
static const unsigned int month = (
    c0 == 'J' // Jan Jun Jul
        ? (c1 == 'a' ? 1 : (c2 == 'n' ? 6 : 7))
    : c0 == 'F' ? 2
    : c0 == 'M' // Mar May
        ? (c2 == 'r' ? 3 : 5)
    : c0 == 'A' // Apr Aug
        ? (c1 == 'p' ? 4 : 8)
    : c0 == 'S' ? 9
    : c0 == 'O' ? 10
    : c0 == 'N' ? 11
    : 12
);

Disclaimer: I just wrote that off the top of my head. It works now, but who knows, maybe I got March wrong.

In fact if you want to get even more fun* we can use arithmetic on some characters:

static const char c0 = __DATE__[0];
static const char c1 = __DATE__[1];
static const char c2 = __DATE__[2];
static const unsigned int month = (
    c0 == 'J' // Jan Jun Jul
        ? (c1 == 'a' ? 1 : (c2 == 'n' ? 6 : 7))
    : c0 == 'M' // Mar May
        ? (3 + (c2 == 'y') * 2)
    : c0 == 'A' // Apr Aug
        ? (4 + (c1 == 'u') * 4)
    : c0 == 'S' ? 9
    : c0 <= 'F' ? (12 - (c0 - 'D') * 5) // Feb, Dec
    : (11 + 'N' - c0) // Oct, Nov
);

*: by "fun" I mean: hated by other developers

Since these are const, you can then use it with templates. For example, suppose we've got a contract job which ends in November, and we want to be sure we'll get brought back in for a few days at a high rate once it's over:

#include <iostream>
using namespace std;

static const unsigned int month = ...;

template <int n> class mm {
public:
    static int v;
};

template<> int mm<9>::v=3; // still employed
template<> int mm<10>::v=2; // let's not be too suspicious
template<> int mm<11>::v=1; // patience...
// no value for December - boom! we're in the money! Just in time for Christmas!

int main() {
    std::cout << mm<month>::v;
    return 0;
}

Finally, if you don't want to be littering the global scope, you should use a constexpr function:

static constexpr int getMonth( void ) {
    const char c0 = __DATE__[0];
    const char c1 = __DATE__[1];
    const char c2 = __DATE__[2];
    return (
        c0 == 'J' // Jan Jun Jul
            ? (c1 == 'a' ? 1 : (c2 == 'n' ? 6 : 7))
        : c0 == 'F' ? 2
        : c0 == 'M' // Mar May
            ? (c2 == 'r' ? 3 : 5)
        : c0 == 'A' // Apr Aug
            ? (c1 == 'p' ? 4 : 8)
        : c0 == 'S' ? 9
        : c0 == 'O' ? 10
        : c0 == 'N' ? 11
        : 12
    );
}

...

std::cout << mm<getMonth()>::v;
Dave
  • 44,275
  • 12
  • 65
  • 105
  • Thank you for your answer, but I really need it in compile time – relaxxx Sep 06 '15 at 14:25
  • My answer is compile time, I'm just suggesting it's a silly thing to do and linked to a saner way to do it at runtime. – Dave Sep 06 '15 at 14:26
  • Oh I see, my mistake... But I can't use it in template parameter (with the same message as original question). So I can't use it – relaxxx Sep 06 '15 at 14:29
  • @relaxxx you can use it in a template. See my latest edit. I've verified this works in ideone, and it should work in any compiler (since the int is const and uses only basic arithmetic / conditionals) – Dave Sep 06 '15 at 14:46
  • I added as simple template to your original code as you did, but gets that error in MSVC. Could you please share ideone link, so I can try exactly your code? – relaxxx Sep 06 '15 at 14:50
  • Ah, I see you're using MSVC2012. That won't support much, and constexpr isn't supported until MSVC2015. In fact I'd be very surprised if you could get this type of compile time parsing to work by any means in MSVC2012. Is there a reason you can't update? – Dave Sep 06 '15 at 14:56
  • unfortunately constexpr is not supported in msvc :( but whe I get rid of it, still the same error... Maybe compiler bug – relaxxx Sep 06 '15 at 14:57
  • I think your compiler is simply too old for this sort of abuse. Update, if you can - 2013 is likely to do much better, and 2015 (if it's available yet) adds official support for constexpr. ideone uses GCC internally, which has a much more rapid release cycle. – Dave Sep 06 '15 at 15:00
  • thanks to your answer I managed to achieve what I wanted - check if there is some "TODO" message in code (we strictly add date to it, and writing TODO message to the compiler output) older than a month (roughly)... it works on ideone http://ideone.com/PBqKOJ ... unfortunately MSVC13 gives `expected constant expression` in static_assert ... but that is for different question... thank you very much – relaxxx Sep 06 '15 at 15:35
3

Just having fun with this here ...

My answer requires C++14 and a few external libs, but demonstrates that fairly amazing compile-time computations are available in C++14.

First I need Scott Schurr's str_const presented at C++ Now 2012. This class is a compile-time string and discussed a bit in this answer.

Then I need this date/time library which is capable of compile-time date and time computations.

Next I need a constexpr implementation of std::find:

template <class InputIterator, class T>
constexpr
inline
InputIterator
find(InputIterator first, InputIterator last, const T& value)
{
    for (; first != last; ++first)
        if (*first == value)
            break;
    return first;
}

With that I can write str_to_month which takes a str_const and turns it into a date::month:

constexpr
date::month
str_to_month(const str_const& m)
{
    constexpr
    str_const months[]
    {
        "Jan", "Feb", "Mar", "Apr", "May", "Jun",
        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
    };
    auto i = ::find(std::begin(months), std::end(months), m);
    if (i == std::end(months))
        throw std::range_error("str_to_month received out of range argument " +
                               std::string(m));
    return date::month{static_cast<unsigned>(i - std::begin(months)) + 1};
}

Next I need a utility to convert a str_const into an int:

constexpr
int
str_to_int(const str_const& s)
{
    int r = 0;
    auto i = s.begin();
    for (; i != s.end() && *i == ' '; ++i)
        ;
    for (; i != s.end(); ++i)
    {
        r *= 10;
        r += *i - '0';
    }
    return r;
}

(with minimal error checking)

And finally I can use these utilities to turn a str_const into a date::year_month_day:

// Assume the form used by __DATE__: Mmm dd yyyy
constexpr
date::year_month_day
str_to_year_month_day(const str_const& s)
{
    return str_to_month(s.substr(0, 3))
          /str_to_int(s.substr(4, 2))
          /str_to_int(s.substr(7));
}

I just exercised all this with the following main, which computes everything with constexpr, and confirms the computation with static_assert:

int
main()
{
    constexpr auto ymd = str_to_year_month_day(__DATE__);
    using namespace date;
    static_assert(ymd == sep/6/2015, "");
    constexpr auto ymwd = year_month_weekday{ymd};
    static_assert(ymwd == sun[1]/sep/2015, "");
}

I compiled this program on Sep. 6, 2015 which happens to be the first Sunday of this month.

You'll need gcc or clang to do this. Even the latest VS-2015 isn't up to spec with constexpr enough to do these computations at compile-time.

Community
  • 1
  • 1
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • Wow, I was unaware that looping in constexpr was allowed. Wasn't the whole idea to avoid the halting problem at compile-time? – Dave Sep 08 '15 at 07:43
  • C++11 forbids looping in constexpr. C++14 does not. If the C++14 compiler can not complete the loop at compile time, it will execute the function at run time, unless the result is used to construct a constexpr object. In the latter case a compile-time error will result. – Howard Hinnant Sep 08 '15 at 14:02
  • Nit: constexpr implies inline – TemplateRex Sep 14 '15 at 06:01
  • Btw, for clang, `-fconstexpr-steps=N` will crank up the number of compile computations to `N` (defaults to 1MiB IIRC) – TemplateRex Sep 14 '15 at 06:14
  • 2
    @TemplateRex: Re: constexpr implies inline. Yes I know. I'm still developing a style on that. My current favorite is to specify both `constexpr` and `inline`, on separate lines. Rationale: I have to port a lot of code to Visual Studio. And even on VS-2015 I find myself having to disable the `constexpr` with a macro. When I do, having `inline` there greatly eases the port. – Howard Hinnant Sep 14 '15 at 14:22
0

C++14 allows you to do this with a proper switch-case statement, which is easier to work with than abusing the ternary conditional operator:

static constexpr uint8_t get_month(const char date[])
{
    switch (date[0])
    {
        case 'J':
            if      (date[1] == 'a') return 1;  //Jan
            else if (date[2] == 'n') return 6;  //Jun
            else                     return 7;  //Jul
        case 'M':
            if (date[2] == 'r') return 3;  //Mar
            else                return 5;  //May
        case 'A':
            if (date[1] == 'p') return 4;  //Apr
            else                return 8;  //Aug
        case 'F': return 2;    //Feb
        case 'S': return 9;    //Sep
        case 'O': return 10;   //Oct
        case 'N': return 11;   //Nov
        case 'D': return 12;   //Dec
        default:  return 0;    //Error
    }

You can call it like get_month(__DATE__), or get rid of the parameter and just use __DATE__ directly.

Adam Haun
  • 359
  • 7
  • 13