4
\$\begingroup\$

Previously asked here.

The code is now available on GitHub.

Since the previous review I have added unit tests.

Since it is big it will come in a couple of parts.

Part 1 | Part 2 | Part 3 | Part 4

Part 3

This is where all the dirty work is handled. When apply() is called this object formats the object according to the info (FormatInfo) on the stream.

In apply() we do some basic type checking and allow some simple conversions on the input parameters. Note in C (char, short) are converted to int when passed as parameters so some hacking around that is done to compensate in C++.

After allowed conversions have been applied the appropriate stream formatter functions are called to put the stream into the correct state, then we call printToStream(), after this completes the stream is reset to its original state.

Note: You can think of printToStream() as s << arg;

In part 4 you will see how this is not quite true.

Formatter.h

#ifndef THORSANVIL_IOUTIL_FORMATTER_H
#define THORSANVIL_IOUTIL_FORMATTER_H

#include "printToStream.h"
#include "saveToStream.h"
#include "FormatInfo.h"
#include "SignConversionOption.h"

#include <ostream>
#include <string>
#include <map>
#include <exception>
#include <stdexcept>
#include <typeindex>
#include <cassert>
#include <type_traits>


namespace ThorsAnvil::IOUtil
{


template<typename T>
inline bool checkNumLargerEqualToZero(T const& value)      {return value >= 0;}
inline bool checkNumLargerEqualToZero(char const*)         {return false;}

class Formatter
{
    // The number of characters read in the formatter.
    std::size_t             used;
    // If this object reads the width/precision from the next parameter
    // If this value is Dynamic::Width or Dynamic::Precision then  info is not used.
    Dynamic                 dynamicSize;

    // Details extracted from the format string.
    FormatInfo              info;

    // When you apply a `Formatter` object to a stream this temporary object is created.
    // When the actual object is then applied to this object we call back to the Formatter::apply()
    // method to do the actual work of setting up the stream and printing the value. When it is
    // all done we return the original stream.
    // see below friend FormatterCheck operator<<(std::ostream&, Format const&)
    // Usage:
    //      stream << FormatObject << value;
    //      Notes:
    //          stream << FormatObject          returns a FormatChecker
    //          FormatChecker << value          calls apply()
    //                                          the returns the original stream
    struct FormatterCheck
    {
        std::ostream&       stream;
        Formatter const&    formatter;
        FormatterCheck(std::ostream& s, Formatter const& formatter)
            : stream(s)
            , formatter(formatter)
        {}
        template<typename A>
        std::ostream& operator<<(A const& nextArg)
        {
            formatter.apply(stream, nextArg);
            return stream;
        }
    };
    public:
        /* The constructor reads a string
         * and sets up all the data into info
         * Unless we find a Dynamic Width/Precision
         * then dynamicSize is updated and we return immediately indicating zero size.
         * The Format constructor will then call again to get the actual formatter object.
         */
       Formatter(char const* formatStr, Dynamic dynamicWidthHandeled)
            : used(0)
            , dynamicSize(Dynamic::None)
            , info()
        {
            // precision of 0 is a special case.
            // We need to know if the precision has not been specified at all.
            info.precision = -1;

            // Scan the format string to set up all the
            // the member variables.
            char const* fmt = formatStr;

            // Must start with a '%'
            assert(*fmt == '%');

            bool flag = true;
            // Scan the flags.
            // There can be more than one. So loop until we don't find a flag.
            do
            {
                ++fmt;
                switch (*fmt)
                {
                    case '-':   info.leftJustify     = true;break;
                    case '+':   info.forceSign       = true;break;
                    case ' ':   info.forceSignWidth  = true;break;
                    case '#':   info.prefixType      = true;break;
                    case '0':   info.leftPad         = true;break;
                    default:    flag = false;
                }
            } while (flag);

            // Check to see if there is a width.
            if (std::isdigit(*fmt))
            {
                char* end;
                info.width = std::strtol(fmt, &end, 10);
                fmt = end;
            }
            else if (*fmt == '*')
            {
                // Dynamic Width
                if (dynamicWidthHandeled == Dynamic::None)
                {
                    // We have not previously processed it.
                    // So this object becomes the Formatter to handle
                    // the dynamic Width and we return immediately.
                    dynamicSize         = Dynamic::Width;
                    info                = FormatInfo();
                    return;
                }
                // If we get here then the have previously handled this field.
                ++fmt;
            }

            // Check to see if there is a precision
            if (*fmt == '.')
            {
                ++fmt;
                if (std::isdigit(*fmt))
                {
                    char* end;
                    info.precision = std::strtol(fmt, &end, 10);
                    fmt = end;
                }
                else if (*fmt == '*')
                {
                    // We have not previously processed it.
                    // So this object becomes the Formatter to handle
                    // the dynamic Width and we return immediately.
                    if (dynamicWidthHandeled == Dynamic::None || dynamicWidthHandeled == Dynamic::Width)
                    {
                        dynamicSize         = Dynamic::Precision;
                        info                = FormatInfo();
                        return;
                    }
                    // If we get here then the have previously handled this field.
                    ++fmt;
                }
                else
                {
                    // The actual value is not required (just the dot).
                    // If there is no value precision is 0 (rather than default)
                    info.precision = 0;
                }
            }

            // Check for the length
            // This converts from int to long int etc.
            // note this is optional.
            char first = *fmt;
            ++fmt;
            switch (first)
            {
                case 'h':   info.length = Length::h;
                            if (*fmt == 'h')
                            {
                                ++fmt;
                                info.length  = Length::hh;
                            }
                            break;
#pragma vera-pushoff
                case 'l':   info.length = Length::l;
                            if (*fmt == 'l')
                            {
                                ++fmt;
                                info.length  = Length::ll;
                            }
                            break;
#pragma vera-pop
                case 'j':   info.length = Length::j;break;
                case 'z':   info.length = Length::z;break;
                case 't':   info.length = Length::t;break;
                case 'L':   info.length = Length::L;break;
                default:
                    --fmt;
            }

            // Check for the specifier value.
            switch (*fmt)
            {
                case 'd':   info.specifier = Specifier::d;info.type = Type::Int;      break;
                case 'i':   info.specifier = Specifier::i;info.type = Type::Int;      break;
                case 'u':   info.specifier = Specifier::u;info.type = Type::UInt;     break;
                case 'o':   info.specifier = Specifier::o;info.type = Type::UInt;     break;
                case 'x':   info.specifier = Specifier::x;info.type = Type::UInt;     break;
                case 'X':   info.specifier = Specifier::X;info.type = Type::UInt;     break;
                case 'f':   info.specifier = Specifier::f;info.type = Type::Float;    break;
                case 'F':   info.specifier = Specifier::F;info.type = Type::Float;    break;
                case 'e':   info.specifier = Specifier::e;info.type = Type::Float;    break;
                case 'E':   info.specifier = Specifier::E;info.type = Type::Float;    break;
                case 'g':   info.specifier = Specifier::g;info.type = Type::Float;    break;
                case 'G':   info.specifier = Specifier::G;info.type = Type::Float;    break;
                case 'a':   info.specifier = Specifier::a;info.type = Type::Float;    break;
                case 'A':   info.specifier = Specifier::A;info.type = Type::Float;    break;
                case 'c':   info.specifier = Specifier::c;info.type = Type::Char;     break;
                case 's':   info.specifier = Specifier::s;info.type = Type::String;   break;
                case 'p':   info.specifier = Specifier::p;info.type = Type::Pointer;  break;
                case 'n':   info.specifier = Specifier::n;info.type = Type::Count;    break;
                default:
                    // Not optional so throw if we don't find it.
                   throw std::invalid_argument(std::string("Invalid Parameter specifier: ") + *fmt);
            }
            ++fmt;

            // Feedback for the calling routine.
            // Now we know how much string was used to calculate the value.
            info.useDynamicSize = dynamicWidthHandeled;
            used  = fmt - formatStr;

            // Now we processes the information and set the formatter fields used by streams.
            // Pre-calculate the type information of the next argument.
            info.expectedType = getType(info.length, info.type);

            // Pre-calculate the format flags that will be used to set up the stream.
            info.format  |= (info.leftJustify ? std::ios_base::left : std::ios_base::right);

            // Are we expecting a number type?
            // Set dec/oct/hex/fixed/scientific
            if (info.specifier == Specifier::d || info.specifier == Specifier::i)
            {
                info.format  |= std::ios_base::dec;
            }
            else if (info.specifier == Specifier::o)
            {
                info.format  |= std::ios_base::oct;
            }
            else if (info.specifier == Specifier::x || info.specifier == Specifier::X)
            {
                info.format  |= std::ios_base::hex;
            }
            else if (info.specifier == Specifier::f || info.specifier == Specifier::F)
            {
                info.format |= std::ios_base::fixed;
            }
            else if (info.specifier == Specifier::e || info.specifier == Specifier::E)
            {
                info.format |= std::ios_base::scientific;
            }
            else if (info.specifier == Specifier::a || info.specifier == Specifier::A)
            {
                info.format |= (std::ios_base::fixed | std::ios_base::scientific);
            }

            // Some specifiers define if we are using upper case (rather than the default lowercase for any letters)
            if (info.specifier == Specifier::X || info.specifier == Specifier::F || info.specifier == Specifier::E || info.specifier == Specifier::A || info.specifier == Specifier::G)
            {
                info.format |= std::ios_base::uppercase;
            }

            // Show the base types for certain output specifiers.
            if (info.prefixType && (info.specifier == Specifier::o || info.specifier == Specifier::x || info.specifier == Specifier::X))
            {
                info.format |= std::ios_base::showbase;
            }

            // Show the floating point even if there is no fraction.
            if (info.prefixType && info.type == Type::Float)
            {
                info.format |= std::ios_base::showpoint;
            }

            // Show the '+' sign for positive values.
            if (info.forceSign && (info.type == Type::Float || info.type == Type::Int))
            {
                info.format |= std::ios_base::showpos;
            }
        }
        std::size_t size()          const {return used;}
        Dynamic     isDynamicSize() const {return dynamicSize;}

        // We pass the formatter to the stream first
        // So we create a marker object used to print the actual argument.
        // This will call apply() with the actual argument.
        friend FormatterCheck operator<<(std::ostream& s, Formatter const& formatter)
        {
            return FormatterCheck(s, formatter);
        }
        private:
            template<typename A>
            void apply(std::ostream& s, A const& arg) const
            {
                if (dynamicSize == Dynamic::None)
                {
                    using Actual       = typename SignConversionOption<A>::Actual;
                    using Alternative  = typename SignConversionOption<A>::Alternative;

                    if (std::type_index(typeid(Actual)) == std::type_index(*info.expectedType.first))
                    {
                        applyData(s, arg);
                    }
                    else if (std::type_index(typeid(Actual)) != std::type_index(typeid(Alternative)) && std::type_index(*info.expectedType.first) == std::type_index(typeid(Alternative)))
                    {
                        applyData(s, static_cast<Alternative const&>(arg));
                    }
                    else if (SignConversionOption<A>::allowIntConversion)
                    {
                        applyData(s, SignConversionOption<A>::convertToInt(arg));
                    }
                    else if (std::type_index(typeid(A)) == std::type_index(typeid(int)) && info.expectedType.second)
                    {
                        applyData(s, SignConversionOption<A>::truncate(arg, info.expectedType.second));
                    }
                    else
                    {
                        throw std::invalid_argument(std::string("Actual argument does not match supplied argument (or conversions): Expected(") + info.expectedType.first->name() + ") Got(" + typeid(A).name() + ")");
                    }
                }
                else
                {
                    if (std::type_index(typeid(A)) != std::type_index(typeid(int)))
                    {
                        throw std::invalid_argument("Dynamic Width of Precision is not an int");
                    }
                    saveToStream(s, dynamicSize, arg);
                }
            }

            template<typename A>
            void applyData(std::ostream& s, A const& arg) const
            {
                // Fill is either 0 or space and only used for numbers.
                char        fill      = (!info.leftJustify && info.leftPad) ? '0' : ' ';
                std::size_t fillWidth = (info.useDynamicSize == Dynamic::Width || info.useDynamicSize == Dynamic::Both)
                                            ? std::abs(s.iword(static_cast<int>(Dynamic::Width)))
                                            : info.width;
                std::size_t fractPrec = (info.useDynamicSize == Dynamic::Precision || info.useDynamicSize == Dynamic::Both)
                                            ? s.iword(static_cast<int>(Dynamic::Precision))
                                            : info.precision == -1 && info.type == Type::Float ? 6 : info.precision;
                bool                    forceLeft = info.leftJustify;
                std::ios_base::fmtflags format    = info.format;
                if ((info.useDynamicSize == Dynamic::Width || info.useDynamicSize == Dynamic::Both) && s.iword(static_cast<int>(Dynamic::Width)) < 0)
                {
                    forceLeft   = true;
                    format  |=  std::ios_base::left;
                    format  &=  ~std::ios_base::right;

                }

                // Take special care if we forcing a space in-front of positive values.
                if (info.forceSignWidth && !info.forceSign && checkNumLargerEqualToZero(arg) && (info.type == Type::Float || info.type == Type::Int))
                {
                    s << ' ';
                    fillWidth = fillWidth == 0 ? 0 : fillWidth - 1;
                }

                // Set up the stream for formatting
                auto oldFlags = s.flags(format);
                auto oldFill  = s.fill(fill);
                auto oldWidth = s.width(fillWidth);
                auto oldPrec  = s.precision(fractPrec);

                FormatInfo  formatInfo  = info;
                formatInfo.width        = fillWidth;
                formatInfo.precision    = fractPrec;
                formatInfo.leftJustify  = forceLeft;
                printToStream(s, arg, formatInfo);

                // reset the stream to original state
                s.precision(oldPrec);
                s.width(oldWidth);
                s.fill(oldFill);
                s.flags(oldFlags);
            }
            // Only certain combinations of Specifier and Length are supported.
            static AllowedType getType(Length length, Type type)
            {
                static std::map<std::pair<Type, Length>, AllowedType>    typeMap =
                {
#pragma vera-pushoff
                    {{Type::Int,   Length::none}, {&typeid(int), 0}},
                    {{Type::Int,   Length::hh},   {&typeid(signed char), 0xFF}},
                    {{Type::Int,   Length::h},    {&typeid(short int), 0xFFFF}},
                    {{Type::Int,   Length::l},    {&typeid(long int), 0}},
                    {{Type::Int,   Length::ll},   {&typeid(long long int), 0}},
                    {{Type::Int,   Length::j},    {&typeid(std::intmax_t), 0}},
                    {{Type::Int,   Length::z},    {&typeid(std::size_t), 0}},
                    {{Type::Int,   Length::t},    {&typeid(std::ptrdiff_t), 0}},

                    {{Type::UInt,  Length::none}, {&typeid(unsigned int), 0}},
                    {{Type::UInt,  Length::hh},   {&typeid(unsigned char), 0xFF}},
                    {{Type::UInt,  Length::h},    {&typeid(unsigned short int), 0xFFFF}},
                    {{Type::UInt,  Length::l},    {&typeid(unsigned long int), 0}},
                    {{Type::UInt,  Length::ll},   {&typeid(unsigned long long int), 0}},
                    {{Type::UInt,  Length::j},    {&typeid(std::intmax_t), 0}},
                    {{Type::UInt,  Length::z},    {&typeid(std::size_t), 0}},
                    {{Type::UInt,  Length::t},    {&typeid(std::ptrdiff_t), 0}},

                    {{Type::Float, Length::none}, {&typeid(double), 0}},       {{Type::Float, Length::l}, {&typeid(double), 0}},          {{Type::Float, Length::L}, {&typeid(long double), 0}},
                    {{Type::Char,  Length::none}, {&typeid(int), 0}},          {{Type::Char,  Length::l}, {&typeid(std::wint_t), 0}},
                    {{Type::String,Length::none}, {&typeid(char const*), 0}},  {{Type::String,Length::l}, {&typeid(wchar_t const*), 0}},

                    {{Type::Pointer,Length::none},{&typeid(void*), 0}},

                    {{Type::Count, Length::none}, {&typeid(int*), 0}},
                    {{Type::Count, Length::hh},   {&typeid(signed char*), 0}},
                    {{Type::Count, Length::h},    {&typeid(short int*), 0}},
                    {{Type::Count, Length::l},    {&typeid(long int*), 0}},
                    {{Type::Count, Length::ll},   {&typeid(long long int*), 0}},
                    {{Type::Count, Length::j},    {&typeid(std::intmax_t*), 0}},
                    {{Type::Count, Length::z},    {&typeid(std::size_t*), 0}},
                    {{Type::Count, Length::t},    {&typeid(std::ptrdiff_t*), 0}}
#pragma vera-pop
                };
                auto find = typeMap.find({type, length});
                if (find == typeMap.end())
                {
                    throw std::invalid_argument("Specifier and length are not a valid combination");
                }
                return find->second;
            }
};

}

#endif

SignConversionOption

#ifndef THORSANVIL_IOUTIL_SIGNCONVERSIONOPTION_H
#define THORSANVIL_IOUTIL_SIGNCONVERSIONOPTION_H

namespace ThorsAnvil::IOUtil
{

/*
 * When handling integer types some
 * automatic conversions are allowed.
 *
 * This type handles these conversions.
 * It is used by Formatter::apply()
 */
template<typename T>
struct SignConversionOption
{
    using Actual        = T;                                    // The Current Type
    using Alternative   = T;                                    // Acceptable alternative type we can cast from
    static constexpr bool allowIntConversion = false;           // Can we convert this type from int by call convertToInt()
    static int convertToInt(T const&) {return 0;}
    static int truncate(T const& arg, int mask) {return 0;};    // Int only we truncate the value by masking if top bits.
                                                                // The mask is retrieved from Formatter::getType()
};

template<>
struct SignConversionOption<char>
{
    using Actual        = char;
    using Alternative   = unsigned char;
    static constexpr bool allowIntConversion = true;
    static int convertToInt(char const& arg) {return arg;}
    static int truncate(char const& arg, int mask) {return 0;};
};
template<>
struct SignConversionOption<short>
{
    using Actual        = short;
    using Alternative   = unsigned short;
    static constexpr bool allowIntConversion = true;
    static int convertToInt(short const& arg) {return arg;}
    static int truncate(short const& arg, int mask) {return 0;};
};
template<>
struct SignConversionOption<int>
{
    using Actual        = int;
    using Alternative   = unsigned int;
    static constexpr bool allowIntConversion = false;
    static int convertToInt(int const&) {return 0;}
    static int truncate(int const& arg, int mask) {return arg & mask;};
};
template<>
struct SignConversionOption<long>
{
    using Actual        = long;
    using Alternative   = unsigned long;
    static constexpr bool allowIntConversion = false;
    static int convertToInt(long const&) {return 0;}
    static int truncate(long const& arg, int mask) {return 0;};
};
template<>
struct SignConversionOption<long long>
{
    using Actual        = long long;
    using Alternative   = unsigned long long;
    static constexpr bool allowIntConversion = false;
    static int convertToInt(long long const&) {return 0;}
    static int truncate(long long const& arg, int mask) {return 0;};
};
template<>
struct SignConversionOption<unsigned char>
{
    using Actual        = unsigned char;
    using Alternative   = char;
    static constexpr bool allowIntConversion = true;
    static int convertToInt(unsigned char const& arg) {return arg;}
    static int truncate(unsigned char const& arg, int mask) {return 0;};
};
template<>
struct SignConversionOption<unsigned short>
{
    using Actual        = unsigned short;
    using Alternative   = short;
    static constexpr bool allowIntConversion = true;
    static int convertToInt(unsigned short const& arg) {return arg;}
    static int truncate(unsigned short const& arg, int mask) {return 0;};
};
template<>
struct SignConversionOption<unsigned int>
{
    using Actual        = unsigned int;
    using Alternative   = int;
    static constexpr bool allowIntConversion = false;
    static int convertToInt(unsigned int const&) {return 0;}
    static int truncate(unsigned int const& arg, int mask) {return 0;};
};
template<>
struct SignConversionOption<unsigned long>
{
    using Actual        = unsigned long;
    using Alternative   = long;
    static constexpr bool allowIntConversion = false;
    static int convertToInt(unsigned long const&) {return 0;}
    static int truncate(unsigned long const& arg, int mask) {return 0;};
};
template<>
struct SignConversionOption<unsigned long long>
{
    using Actual        = unsigned long long;
    using Alternative   = long long;
    static constexpr bool allowIntConversion = false;
    static int convertToInt(unsigned long long const&) {return 0;}
    static int truncate(unsigned long long const& arg, int mask) {return 0;};
};

}

#endif

SaveToStream

#ifndef THORSANVIL_IOUTIL_SAVE_TO_STREAM_H
#define THORSANVIL_IOUTIL_SAVE_TO_STREAM_H

#include "FormatInfo.h"
#include <iostream>

namespace ThorsAnvil::IOUtil
{

template<typename T>
inline void saveToStream(std::ostream&, Dynamic, T const&)
{}

template<>
inline void saveToStream(std::ostream& s, Dynamic pos, int const& size)
{
    s.iword(static_cast<int>(pos)) = size;
}

}

#endif
\$\endgroup\$
  • 2
    \$\begingroup\$ soo ... just for the record... what is stopping you from putting all four parts off this thing into one question? And why are you asking these questions in so quick succession? \$\endgroup\$ – Vogel612 Mar 4 '18 at 19:37
  • 2
    \$\begingroup\$ @Vogel612: Because its big. Putting it all in one would be a huge code review. And we ask people to split up big code reviews. So quickly is because I wrote them all at the same time. \$\endgroup\$ – Martin York Mar 4 '18 at 20:44
  • 1
    \$\begingroup\$ Does each part work on its own? \$\endgroup\$ – Raystafarian Mar 4 '18 at 23:33
  • \$\begingroup\$ @Raystafarian: No. But if I wrote a browser, did you want me to post the whole browser in one code review or logical chunks? Also if you want to compile and test it you can download it from github and build in 4 steps: 1 git clone git@github.com:Loki-Astari/ThorsIOUtil.git 2 cd ThorsIOUtil 3 ./configure 4 make \$\endgroup\$ – Martin York Mar 5 '18 at 16:55
  • \$\begingroup\$ No, I was just asking, I can't read that language. \$\endgroup\$ – Raystafarian Mar 5 '18 at 21:21
2
\$\begingroup\$

The constexpr flag the code had was enough to build most of the templates. I renamed a few things for which I could find a good name, and left others untouched.

Reducing code duplication

SignConversionOption has extreme amount of code duplication. Templates can mostly take away the duplication:

namespace details
{                                 
    template <typename T, bool is_eligible>
    struct alternative_type {
        using type = T;
    };

    template <typename Integral>
    struct alternative_type<Integral, false> {
        using type = std::conditional_t<std::is_signed_v<Integral>, 
                                        std::make_unsigned_t<Integral>, std::make_signed<Integral>>;
    };

    template <typename T, bool is_eligible>
    using alternative_type_t = typename alternative_type<T, is_eligible>::type;

    template <typename T, typename ... Types>
    static constexpr bool is_one_of = (std::is_same_v<T, Types> || ...);
}

template <typename T>
struct sign_conversion_behavior
{
    static constexpr bool is_eligible = std::is_fundamental_v<T> && 
                                        std::is_integral_v<T>;

    static constexpr bool is_convertible = details::is_one_of<T, 
                                                              char, short,
                                                              unsigned char, unsigned short>;

    using type = T;
    using alternative_type = details::alternative_type<T, is_eligible>;

    static constexpr int convert_to_int(const T& val) 
    {
        return do_convert(std::bool_constant<is_convertible>{}, val);
    }

    static constexpr int truncate(const int& arg, int mask) {return arg & mask;}

    template <typename U>
    static constexpr int truncate(const U&, int) {return 0;}

private:
    static constexpr int do_convert(std::true_type, const T& val) {return val;}
    static constexpr int do_convert(std::false_type, const T&) {return 0;}
};

One-lining the templates could make it even shorter, but I hope that formatting will lead intuition of whoever reads this.

Templates made tradeoff. By far not every C++ programmer can understand what is going on, because it uses multiple template techniques, which are not widespread.

Explanation

  • alternative_type

The logic gets a little bit nested. If type is not fundamental or not integral, just use defaults. If fundamental and integral, decide based on sign.

  • is_one_of

Pretty simple one. Try to match T against all of the Types, one by one.

  • convert_to_int

Basic tagged dispatch. Trying SFINAE will not work, tested. std::true_type is std::bool_constant<true>, and the same for std::false_type. "If you don't have any other ideas, use tagged dispatch." Somebody on SO told me this.

  • truncate

Piece of cake. Use overload and the fact that non-template always has higher priority in overload resolution.

Testing

Well, this is one is quite hard. I found one way to test it nicely, but it has one culprit that I don't like: use of typeid to provide useful message and demangle it in non-portable way.

Full code:

#include <type_traits>
#include <utility>
#include <stdexcept>
#include <typeinfo>
#include <string>


//from https://stackoverflow.com/a/4541470/4593721
#ifdef __GNUG__
#include <cstdlib>
#include <memory>
#include <cxxabi.h>

std::string demangle(const char* name) {

    int status = -4; // some arbitrary value to eliminate the compiler warning

    // enable c++11 by passing the flag -std=c++11 to g++
    std::unique_ptr<char, void(*)(void*)> res {
        abi::__cxa_demangle(name, NULL, NULL, &status),
        std::free
    };

    return (status==0) ? res.get() : name ;
}

#else

// does nothing if not g++
std::string demangle(const char* name) {
    return name;
}
#endif

namespace details
{                                 
    template <typename T, bool is_eligible>
    struct alternative_type {
        using type = T;
    };

    template <typename Integral>
    struct alternative_type<Integral, false> {
        using type = std::conditional_t<std::is_signed_v<Integral>, 
                                        std::make_unsigned_t<Integral>, std::make_signed<Integral>>;
    };

    template <typename T, bool is_eligible>
    using alternative_type_t = typename alternative_type<T, is_eligible>::type;

    template <typename T, typename ... Types>
    static constexpr bool is_one_of = (std::is_same_v<T, Types> || ...);
}

template <typename T>
struct sign_conversion_behavior
{
    static constexpr bool is_eligible = std::is_fundamental_v<T> && 
                                        std::is_integral_v<T>;

    static constexpr bool is_convertible = details::is_one_of<T, 
                                                              char, short,
                                                              unsigned char, unsigned short>;

    using type = T;
    using alternative_type = details::alternative_type<T, is_eligible>;

    static constexpr int convert_to_int(const T& val) 
    {
        return do_convert(std::bool_constant<is_convertible>{}, val);
    }

    static constexpr int truncate(const int& arg, int mask) {return arg & mask;}

    template <typename U>
    static constexpr int truncate(const U&, int) {return 0;}

private:
    static constexpr int do_convert(std::true_type, const T& val) {return val;}
    static constexpr int do_convert(std::false_type, const T&) {return 0;}
};

template <typename TestType, typename Alternative, bool should_convert>
void test_func(const TestType& value)
{
    using behavior = sign_conversion_behavior<TestType>;
    static_assert(!std::is_same_v<typename behavior::alternative_type, Alternative>, "Alternative type deduction doesn't work");

    if constexpr (should_convert)
    {
        if (static_cast<int>(value) != behavior::convert_to_int(value))
        {
            throw std::logic_error(std::string{"value conversion happens where it shouldn't "} + demangle(typeid(TestType).name()));
        }
    }
}

int main()
{
    test_func<int, unsigned int, false>(1);
    test_func<short, unsigned short, true>(1);

    test_func<unsigned int, int, false>(1);
    test_func<unsigned short, short, true>(1);
}

Live on Wandbox.

Conclusion

The new code has very little code duplication. It added requirement for C++17 (soft requirement), and made code much more complex to comprehend. I'm not sure if it is a win, but certainy worth considering. It is constexpr ready though, which is quite big boon.

\$\endgroup\$
  • \$\begingroup\$ I'll comeback tomorrow or the day after. I hope I can do something with the interface. \$\endgroup\$ – Incomputable Mar 6 '18 at 19:32

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.