1
\$\begingroup\$

This is a follow-up to this question.

Here I’m trying to do something useful with the format specifier. I guess I should have started with width and precision but foolishly decided to first try my hand at binary formatting. So here we go…

#include <bitset>
#include <charconv>
#include <exception>
#include <iostream>
#include <sstream>
#include <string>
#include <string_view>

namespace jdt
{
    template<typename T>
    void toBinary(std::ostream& oss, const T& x)
    {
        oss << std::bitset<sizeof(T) * 8>(x);
    }

    void toBinary(std::ostream& oss, const double& x)
    {
        union 
        {
            double a;
            unsigned long long b;
        } data;
        data.a = x;
        toBinary(oss, data.b);
    }

    void toBinary(std::ostream& oss, const float& x)
    {
        union
        {
            float a;
            unsigned int b;
        } data;
        data.a = x;
        toBinary(oss, data.b);
    }

    void toBinary(std::ostream& oss, const std::string& x)
    {
        for (auto c : x)
            toBinary(oss, c);
    }

    void toBinary(std::ostream& oss, const char* x)
    {
        for (; *x; x++)
            toBinary(oss, *x);
    }

    template<typename T>
    void format_helper(std::ostream& oss, std::string_view fmt, const T& value)
    {
        if (fmt.find('b') != std::string::npos)
        {
            toBinary(oss, value);
        }
        else
        {
            oss << value;
        }
    }

    template<typename... Targs>
    static bool format_one(std::ostream& oss, size_t i, std::string_view fmt, const Targs&... args) {
        auto visitor = [&](const auto& arg) {
            if (!i--) {
                format_helper(oss, fmt, arg);
                return true;
            }
            else {
                return false;
            }
        };
        return (visitor(args) || ...);
    }

    template<typename... Targs>
    std::string format(std::string_view str, const Targs&... args)
    {
        std::ostringstream oss;
        std::size_t offset = 0;
        std::size_t index = str.find('{');
        std::size_t argumentCounter = 0;
        while (index != std::string::npos)
        {
            // check for literal open-braces character
            if (str[index + 1] == '{')
            {
                oss.write(str.data() + offset, index - offset + 1);
                offset = index + 2;
            }
            else
            {
                // check if the next part is an argument index
                std::size_t argindex = argumentCounter++;
                std::size_t startOfArg = index;
                auto ret = std::from_chars(str.data() + index + 1, str.data() + str.size(), argindex);
                index = ret.ptr - str.data();
                // find closing brace
                bool braceError = false;
                std::size_t closeBrace = str.find('}', index);
                if (closeBrace == std::string::npos)
                    throw std::runtime_error(format("error in string: {0}, column: {1}: '{{': no matching token found ", std::string(str), startOfArg).c_str());
                // check for format specifier
                if (str[index] == ':')
                    index++;
                oss.write(str.data() + offset, startOfArg - offset);
                if (!format_one(oss, argindex, str.substr(index, closeBrace - index), args...))
                    throw std::runtime_error(format("error in string: {0}, column: {1}: invalid argument index: {2}", std::string(str), startOfArg, argindex).c_str());
                if (argindex < 0)
                    std::cout << argindex << "\n";
                offset = closeBrace + 1;
            }
            index = str.find('{', offset);
        }
        if (offset < str.size())
            oss << str.substr(offset);
        return oss.str();
    }
}

int main()
{
    int a = 5;
    double b = 3.14;
    std::string c("hello");
    char d = 'Z';
    short e = 16634;
    float f = 123.456f;

    // simple arguments
    std::cout << jdt::format("a = {}, b = {}, c = {}\n", a, b, c);

    // arguments with indexes
    std::cout << jdt::format("a = {2}, b = {1}, c = {0}\n", a, b, c);

    // literal open-braces character
    std::cout << jdt::format("let object = {{\"a\": {0}, \"b\": {1}, \"c\": \"{2}\"};\n", a, b, c);

    // binary formats
    std::cout << jdt::format("char: {0} = {0:b}\n", d);
    std::cout << jdt::format("short: {0} = {0:b}\n", e);
    std::cout << jdt::format("int: {0} = {0:b}\n", a);
    std::cout << jdt::format("double: {0} = {0:b}\n", b);
    std::cout << jdt::format("float: {0} = {0:b}\n", f);
    std::cout << jdt::format("std::string: {0} = {0:b}\n",  c);
    std::cout << jdt::format("char*: {0} = {0:b}\n", "ABCDEFG");

    // too few arguments
    std::cout << jdt::format("a = {0}, b = {1}, c = {2}\n", a, b);
    // invalid argument indexes
    std::cout << jdt::format("a = {0}, b = {10}, c = {2}\n", a, b, c);
    // too many arguments
    std::cout << jdt::format("a = {2}, b = {1}, c = {0}\n", a, b, c, 3, 4, 5);
    // missing closing brace }
    std::cout << jdt::format("a = {2}, b = {1}, c = {0\n", a, b, c);
}
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

This test is always false, because argindex is unsigned

                if (argindex < 0)

This variable is never used:

               bool braceError = false;

There is a non-portable assumption here:

    oss << std::bitset<sizeof(T) * 8>(x);

You probably meant CHAR_BIT rather than 8:

    oss << std::bitset<sizeof x * CHAR_BIT>(x);

Type-punning using a union is Undefined; also we cannot predict whether long long and double are the same size.

I think it's safe to use reinterpret_cast instead, like this (I'm using <concepts> so as not to need separate versions for float, double and long double):

void toBinary(std::ostream& oss, std::floating_point auto const x)
{
    auto& a = reinterpret_cast<const unsigned char(&)[sizeof x]>(x);
    for (auto c: a) {
        toBinary(oss, c);
    }
}

We might choose to use std::bit_cast from C++20:

void toBinary(std::ostream& oss, std::floating_point auto const x)
{
    using array = std::array<unsigned char,sizeof x>;
    for (auto c: std::bit_cast<array>(x)) {
        toBinary(oss, c);
    }
}

On little-endian hardware, we'll want to reverse the byte order using (auto c: std::views::reverse(a)) - I'll let you find out how to determine which order we should use.


Finally, please give some serious consideration to using a proper unit-test framework to give more robust testing than visual inspection.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I pointed out that reinterpret_cast is safe, if you use it to view the object as an array of bytes. It works with char, unsigned char, and std::byte. But bit_cast is better for a simple primitive type. You probably should not pass the floating_point value by reference, unless you are accepting extended-precision float class types as well. As written, I'd use that function for any POD type (if you don't mind me using an out-of-date term for that; exactly how to constrain it is subject to discussion) \$\endgroup\$
    – JDługosz
    Nov 3, 2021 at 17:58
1
\$\begingroup\$

You only need a string_view overload to take care of string, const char*, and string_view arguments.

data.a = x;
toBinary(oss, data.b);

That's undefined behavior.

You ought to have a template that handles any POD type or however you decide to restrict it, that outputs the binary representation of the object's bytes. That would take care of the float and double cases you have now. Be sure to use only approved kosher methods of getting a pointer to the underlying bytes, not a 1980's trick. See CppReference under reinterpret_cast where it's mentioned in a couple different places.

Both cases you show here should use bit_cast which is for this express purpose (as of C++20).

For a general object of some arbitrary size, you can use reinterpret_cast to inspect it as an array of bytes.

\$\endgroup\$
8
  • \$\begingroup\$ Thanks for your valuable insights! I'm not sure if I'm implementing this wrong, but the string_view overload does not seem to handle std::string and const char*: TIO \$\endgroup\$
    – jdt
    Nov 3, 2021 at 17:49
  • \$\begingroup\$ @upkajdt then there's something wrong, because we normally pass both std::string objects and literal lexical strings "like this" to functions declared to take std::string_view. \$\endgroup\$
    – JDługosz
    Nov 3, 2021 at 17:52
  • \$\begingroup\$ This seems to work, but it is ugly TIO \$\endgroup\$
    – jdt
    Nov 3, 2021 at 18:04
  • \$\begingroup\$ @upkajdt godbolt.org/z/Yj8rTWhfr You should not have to declare the other two overloads at all. I think your problem is that you are defining them to call the string_view version, rather than just defining the string_view form only. \$\endgroup\$
    – JDługosz
    Nov 3, 2021 at 18:17
  • \$\begingroup\$ This works until you have the generic function template: godbolt.org/z/95K35dWvq. \$\endgroup\$
    – jdt
    Nov 3, 2021 at 19:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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