mercredi 4 octobre 2017

const_cast seems to be ignored with C++ templates?

I wrote a simple logging class that supports variadic templates in C++ using Visual Studio. I created a generic Log function template with a number of specializations to cater for a common combination of possible inputs.

#pragma once

#include <Windows.h>
#include <locale>
#include <codecvt>
#include <string>
#include <sstream>
#include <utility>

using namespace std;

inline static string to_utf8(const wstring& s) {
    wstring_convert<codecvt_utf8_utf16<wchar_t>> utf16conv;
    return utf16conv.to_bytes(s);
}

class Logger {
public:
    HANDLE file_handle;
    wchar_t file_path[MAX_PATH + 1];

    inline Logger(HANDLE handle) : file_handle(handle), file_path{} {
    }

    inline Logger(const string& path) : Logger(path.c_str()) {
    }

    inline Logger(const wstring& path) : Logger(path.c_str()) {
    }

    inline Logger(const char* path) : file_handle(NULL) {
        wstring_convert<codecvt_utf8_utf16<wchar_t>> converter;
        wcscpy_s(file_path, MAX_PATH + 1, converter.from_bytes(path).c_str());
    }

    inline Logger(const wchar_t* path) : file_handle(NULL) {
        wcscpy_s(file_path, MAX_PATH + 1, path);
    }

private:
    inline void VerifyInitialize() {
        if ((file_handle == NULL || file_handle == INVALID_HANDLE_VALUE) && file_path[0] != '\0') {
            file_handle = CreateFileW(file_path, FILE_GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
            SetFilePointer(file_path, 0, NULL, FILE_END);
        }
    }

public:
    inline void Log() {
    }

    template<typename ...Rest>
    inline void Log(const char* first, Rest... params) {
        VerifyInitialize();
        if (file_handle == NULL || file_handle == INVALID_HANDLE_VALUE)
            return;
        DWORD written;
        WriteFile(file_handle, first, static_cast<DWORD>(strlen(first)), &written, NULL);
        Log(params...);
    }

    template<typename ...Rest>
    inline void Log(const char first, Rest... params) {
        char str[2];
        str[0] = first;
        str[1] = '\0';
        Log(str, params...);
    }

    template<typename ...Rest>
    inline void Log(const string* first, Rest... params) {
        VerifyInitialize();
        if (file_handle == NULL || file_handle == INVALID_HANDLE_VALUE)
            return;
        DWORD written;
        WriteFile(file_handle, first->c_str(), static_cast<DWORD>(first->size()), &written, NULL);
        Log(params...);
    }

    template<typename ...Rest>
    inline void Log(const string& first, Rest... params) {
        Log(&first, params...);
    }

    template<typename ...Rest>
    inline void Log(const wstring* first, Rest... params) {
        Log(*first, params...);
    }

    template<typename ...Rest>
    inline void Log(const wstring& first, Rest... params) {
        Log(to_utf8(first), params...);
    }

    template<typename ...Rest>
    inline void Log(const wchar_t* first, Rest... params) {
        Log(wstring(first), params...);
    }

    template<typename ...Rest>
    inline void Log(const wchar_t first, Rest... params) {
        wchar_t str[2];
        str[0] = first;
        str[1] = '\0';
        Log(str, params...);
    }

    template<typename First, typename ...Rest>
    inline void Log(First first, Rest... params) {
        printf("%s\n", typeid(First).name());
        if (is_const<First>::value) {
            stringstream stream;
            stream << first;
            Log(stream.str(), params...);
        } else
            Log(const_cast<const First>(first), params...);
    }

    inline ~Logger() {
        if (!(file_handle == NULL || file_handle == INVALID_HANDLE_VALUE)) {
            CloseHandle(file_handle);
            file_handle = NULL;
        }
    }
};

The code works fine with const values. If however I introduce non-const parameters like so:

int main() {
    Logger logger(("output.txt"));
    wchar_t sometext[3];
    sometext[0] = '1';
    sometext[1] = '8';
    sometext[2] = '\0';
    logger.Log(sometext, L"\n");
    return 0;
}

the specializations are not called and instead the last generic void Log(First first, Rest... params) is called, which uses stringstream and writes the pointer as a string instead of the string itself.

If I remove const from all overload parameters, when I invoke main() it works, but if I replace sometext with a const char*, then the last generic void Log(First first, Rest... params) is called instead of the specializations (ie. removing const does not solve the problem).

So, in order to try the get the best of both worlds, I tried adding the following condition:

template<typename First, typename ...Rest>
inline void Log(First first, Rest... params) {
    if (is_const<First>::value) {
        stringstream stream;
        stream << first;
        Log(stream.str(), params...);
    } else
         Log(const_cast<const First>(first), params...);
}

with the rationale being to instruct the compiler to "first search for a const specialization overload, and if none was found, then fallback to using stringstream".

However, I was getting a stack overflow. To debug, I added printf("%s\n", typeid(First).name()); just before the if condition, and the output was:

wchar_t * __ptr64
wchar_t * __ptr64
wchar_t * __ptr64
wchar_t * __ptr64
....

Apparently, const_cast<const First>(first) does not seem to be in fact casting to const, even though is_const::value is indeed returning false. I have also tried using std::as_const, but there was no difference in the outcome.

Why is the cast not working? How do I resolve this please?

Aucun commentaire:

Enregistrer un commentaire