7
\$\begingroup\$

While writing simple text rendering I found a lack of utf-8 decoders. Most decoders I found required allocating enough space for decoded string. In worse case that would mean that the decoded string would be four times as large as the original string.

I just needed to iterate over characters in a decoded format so I would be able to render them on the screen, so I wrote a simple function that would allow me to do that:

#include <cstdint>

// unsigned integer types
typedef uint64_t U64;
typedef uint32_t U32;
typedef uint16_t U16;
typedef uint8_t U8;

// signed integer types
typedef int64_t I64;
typedef int32_t I32;
typedef int16_t I16;
typedef int8_t I8;

U32 NextUTF8Char(const char* str, U32& idx)
{
    // https://en.wikipedia.org/wiki/UTF-8
    U8 c1 = (U8) str[idx];
    ++idx;

    U32 utf8c;

    if (((c1 >> 6) & 0b11) == 0b11)
    {
        // at least 2 bytes
        U8 c2 = (U8) str[idx];
        ++idx;
        if ((c1 >> 5) & 1)
        {
            // at least 3 bytes
            U8 c3 = (U8) str[idx];
            ++idx;

            if ((c1 >> 4) & 1)
            {
                // 4 bytes
                U8 c4 = (U8) str[idx];
                ++idx;

                utf8c = ((c4 & 0b00000111) << 18) | ((c3 & 0b00111111) << 12) |
                        ((c2 & 0b00111111) << 6) | (c1 & 0b00111111);
            } else
            {
                utf8c = ((c3 & 0b00001111) << 12) | ((c2 & 0b00111111) << 6) |
                        (c1 & 0b00111111);
            }
        } else
        {
            utf8c = ((c1 & 0b00011111) << 6) | (c2 & 0b00111111);
        }


    } else
    {
        utf8c = c1 & 0b01111111;
    }

    return utf8c;
}

Usage:

const char* text = u8"ta suhi škafec pušča";
U32 idx = 0;
U32 c;
while ((c = NextUTF8Char(text, idx)) != 0)
{
    // c is our utf-8 character in unsigned int format
}

I'm currently mostly concerned about the following :

  • Readability: The intent of every piece of code is clear to the reader.
  • Correctness: Everything is working as it should (I think it's clear what should happen).
  • Performance: Can anything be done to improve the performance of this code?
\$\endgroup\$
3
  • \$\begingroup\$ Looks like you're missing #include <cstdint> and a bunch of usings for those typedefs - would you care to include them in the code? \$\endgroup\$ Apr 6, 2021 at 11:33
  • \$\begingroup\$ @TobySpeight Included the missing header, thank you. Those types are already declared with typedef specifier on top of the function, so I do not know what do you mean by that. \$\endgroup\$
    – KlemenPl
    Apr 6, 2021 at 12:01
  • \$\begingroup\$ I meant the usings that bring std::uint64_t and friends into the global namespace. \$\endgroup\$ Apr 6, 2021 at 13:16

3 Answers 3

4
\$\begingroup\$
// unsigned integer types
typedef uint64_t U64;
typedef uint32_t U32;
typedef uint16_t U16;
typedef uint8_t U8;

// signed integer types
typedef int64_t I64;
typedef int32_t I32;
typedef int16_t I16;
typedef int8_t I8;

This has instantly made the code harder to read (as well as being incorrect, since <cstdint> declares those names in the std namespace). I'm not sure why we declare so many types, when we use just two of them anyway.

U32 NextUTF8Char(const char* str, U32& idx)

Why not return a standard std::wchar_t? Or perhaps a char32_t? Similarly, str ought to be a const char8_t* (so that the example code compiles).

I'd use a std::size_t for the index (or more likely get rid of idx altogether, and pass a reference to pointer instead).

The whole thing seems like a reinventing a lot of work that's already done for you:

#include <cwchar>

char32_t NextUTF8Char(const char8_t*& str)
{
    static const int max_utf8_len = 5;
    auto s = reinterpret_cast<const char*>(str);
    wchar_t c;
    std::mbstate_t state;
    auto len = std::mbrtowc(&c, s, max_utf8_len, &state);
    if (len > max_utf8_len) { return 0; }
    str += len;
    return c;
}
#include <iostream>
int main()
{
    std::locale::global(std::locale{"en_US.utf8"});
    const auto* text = u8"ta suhi škafec pušča";
    char32_t c;
    std::size_t i = 0;
    while ((c = NextUTF8Char(text)) != 0) {
        std::cout << '[' << i++ << "] = " << (std::uint_fast32_t)c << '\n';
        // c is our utf-8 character in unsigned int format
    }
}

I think that std::codecvt<char32_t, char8_t, std::mbstate_t> could easily do much the same:

#include <locale>

char32_t NextUTF8Char(const char8_t*& str)
{
    if (!*str) {
        return 0;
    }
    auto &cvt = std::use_facet<std::codecvt<char32_t, char8_t, std::mbstate_t>>(std::locale());
    std::mbstate_t state;

    char32_t c;
    char32_t* p = &c+1;
    auto result = cvt.in(state, str, str+6, str, &c, p, p);
    switch (result) {
    case std::codecvt_base::ok: return c;
    case std::codecvt_base::partial: return c;
    case std::codecvt_base::error: return 0;
    case std::codecvt_base::noconv: return 0;
    }
    return c;
}

Either is better than writing your own UTF-8 decoder.

\$\endgroup\$
5
  • 3
    \$\begingroup\$ Calling locale or mbrtowc for every single character is much overhead for a transformation that does not (and should not ever) rely on the current locale. Definitely agree with returning char32_t though. \$\endgroup\$ Jan 8, 2023 at 4:43
  • 1
    \$\begingroup\$ Yes, that's true. It's not clear why the review code wants to deal with a codepoint at a time, rather than simply transforming an entire string to UCS-4. And of course, codepoints aren't always complete in themselves if combining characters are involved... \$\endgroup\$ Jan 8, 2023 at 10:45
  • 1
    \$\begingroup\$ mbrtowc and related functions (besides bringing with them significant performance overhead over the straightforward UTF-8 decoder) are extremely inadvisable simply because of their dependence on global locale. You had the foresight to try and set that in main(), but consider that 1. this is not possible when writing a library, 2. you might be forced to use a library that is itself stupidly locale-dependent and 3. "en_US.utf8" might not even exist on your target machine, in which case you're just completely hosed. \$\endgroup\$
    – Tau
    Apr 27, 2023 at 17:22
  • 1
    \$\begingroup\$ Note that wchar_t is only 16 bits wide on MSVC (even though that violates the Standard). The best type for the return value is char32_t. \$\endgroup\$
    – Davislor
    Sep 9, 2023 at 17:45
  • \$\begingroup\$ @Davislor, that's hideous - every day I find a new reason to be glad I never have to support Microsoft platforms! \$\endgroup\$ Sep 12, 2023 at 7:16
7
\$\begingroup\$

Use a String View, not a C-Style String

Currently, you use a C-style interface:

U32 NextUTF8Char(const char* str, U32& idx)

This has the serious flaw that there is no bounds checking on a string of arbitrary length, which is a buffer overrun waiting to happen.

The best type to use to represent a stringy object is a std::string_view, which passes the string and its length around simply with low overhead, and which other stringy types convert to and from efficiently,

Always, always, always check for buffer overruns!

Use char32_t

This is what it’s for! If you’re converting or displaying UCS-4 codepoints, your interface probably expects either char32_t (if it’s portable) or 32-bit wchar_t. The uint_least32_t type from <cstdint> has the same size and alignment, and also would work.

The uint32_t type isn’t completely portable. (It theoretically does not exist on a machine with no exact-width 32-bit type, although I suspect that actual implementations will support it anyway.) A wchar_t won’t work because that’s only 16 bits wide on Windows. (Which violates the Standard, but the original sin of Unicode was thinking 65,536 codepoints would be enough forever if they could just force the Japanese to go along with it, both Microsoft and the C++ Standard Committee believed this, and Microsoft was not going to break the Windows API.) An unsigned long is 64 bits wide on some implementations. An unsigned int is 16 bits wide on some architectures, and 64 bits wide on a few.

Consider An Iterator Interface

Which operations are you doing? You’re retrieving the next codepoint from the string and incrementing the index to the start of the next UTF-8 codepoint. Those are the * and ++ operations of a ForwardIterator! You also implicitly need to compare the position to the end of the string. Comparison of two indices, testing whether or not we are at the end, and swap, are useful operations too. This way, whatever internal state you want to add (such as remembering the end of the substring and checking for overflow, or caching the codepoint at the current position so that you can read it twice without recalculating) is encapsulated in the object and does not need to be part of the external interface.

Implementing the forward_iterator interface lets you use them with every other language feature that takes an iterator, for example:

const std::u32string converted(u32_begin(utf8_source), u32_end(utf8_source));

Or

for( auto it = u32_begin(utf8_source); it; ++it )

Or a Hybrid Interface

When you implement the std::forward_iterator interface, you’ll notice that it’s slower because the dereference (*) and increment (++) duplicate many of the same checks. This is why Rust has a single .next() function that does both. You can implement that in C++ as well, and get something closer to the original code in structure and performance.

There isn’t as much built-in syntax sugar for .next() in C++, but you can write while (it) or (if the string cannot contain zero bytes in the middle) while(auto wc = it.next()) loops. For example:

while(it) {
    test3.push_back(it.next());
}

You Can Optimize the Decoder

Currently, you have nested if-else blocks that sometimes increment the index and retrieve the next character. These do not generate good code for mainstream architectures in 2023.

The fastest approach I’m aware of is a finite-state machine, but it is also possible to write a branchless implementation.

Putting it All Together

Here is a sample implementation as an iterator class:

#include <cassert>
#include <compare> // partial_ordering
#include <cstddef> // ptrdiff_t, size_t
#include <cstring>
#include <iterator> // iterator_category
#include <stdexcept> // logic_error, runtime_error
#include <string_view>

namespace ucs4 {
/* The necessary non-member functions must be friends of the class.  This
 * requires a forward declaration before the class definition, which in turn
 * requires a forward declaration of the class as an incomplete type.
 */
class ucs4_it;
constexpr void swap(ucs4_it&, ucs4_it&) noexcept;

class ucs4_it {
private:
/* Store a view of the substring, not merely a position within it, so as to
 * detect and prevent a buffer overrun.
 */
    static constexpr const char* INVALID_UTF8_MSG = "Invalid UTF-8 data.";

    const char8_t* begin = nullptr;
    std::size_t size = 0;

    constexpr ucs4_it(const char8_t* const new_start, const std::size_t new_size) noexcept
     : begin(new_start), size(new_size)
    {}

public:
    using difference_type = std::ptrdiff_t;
    using value_type = char32_t;
    using pointer = char32_t*;
    using const_ptr = const char32_t*;
    using reference = char32_t&;
    using const_reference = const char32_t&;
    using size_type = std::size_t;
    using iterator_category = std::forward_iterator_tag;

    ucs4_it() = default;
    ucs4_it(const ucs4_it&) = default;
    ucs4_it(ucs4_it&&) = default;
    ucs4_it& operator=(const ucs4_it&) = default;
    ucs4_it& operator=(ucs4_it&&) = default;
    ~ucs4_it() = default;

    constexpr void swap(ucs4_it& other) noexcept {
        ::ucs4::swap(*this, other);
    }

    constexpr operator bool() const noexcept {return size != 0;}

/* Most of the logic of the original implementation goes here: */
    constexpr value_type operator*() const {
/* The default ucs4_it object references an empty string, and can be
 * dereferenced.
 */
        if (size == 0)
          return 0;

        if (!begin) {
/* This is a logic error: it should be impossible to create a stringy object
 * from a null pointer and a nonzero length.
 */
            throw std::logic_error("Invalid ucs4_it object (invalid base, nonzero length).");
        }

/* A not-particularly-optimized implementation with error-checking
 * and low cyclomatic complexity.
 */
        const auto c1 = begin[0];
        if        (c1 < 0b10000000U) {
            return c1;
        } else if (c1 < 0b11000000U) {
            throw std::runtime_error(INVALID_UTF8_MSG);
        } else if (c1 < 0b11100000U && size >= 2U) {
            const auto c2 = begin[1];
            if (c2 < 0b10000000U || c2 >= 0b11000000U) {
                throw std::runtime_error(INVALID_UTF8_MSG);
            }
            return (c1 & 0b00011111U) << 6U |
                   (c2 & 0b00111111U);
        } else if (c1 < 0b11110000U && size >= 3U) {
            const auto c2 = begin[1];
            const auto c3 = begin[2];

            if (c2 < 0b10000000U || c2 >= 0b11000000U ||
                c3 < 0b10000000U || c3 >= 0b11000000U) {
                throw std::runtime_error(INVALID_UTF8_MSG);
            }

            return (c1 & 0b00001111U) << 12U |
                   (c2 & 0b00111111U) << 6U |
                   (c3 & 0b00111111U);
        } else if (c1 < 0b11111000U && size >= 4U) {
            const auto c2 = begin[1];
            const auto c3 = begin[2];
            const auto c4 = begin[3];

            if (c2 < 0b10000000U || c2 >= 0b11000000U ||
                c3 < 0b10000000U || c3 >= 0b11000000U ||
                c4 < 0b10000000U || c4 >= 0b11000000U )
                {
                throw std::runtime_error(INVALID_UTF8_MSG);
            }

            return (c1 & 0b00000111U) << 18UL | // Unsigned int operands might overflow on a 16-bit architecture.
                   (c2 & 0b00111111U) << 12U |
                   (c3 & 0b00111111U) << 6U |
                   (c4 & 0b00111111U);
        } else {
            throw std::runtime_error(INVALID_UTF8_MSG);
        }
    }

    constexpr ucs4_it& operator++() {
        if (size == 0) {
          return *this;
        }

        if (!begin) {
/* This is a logic error: it should be impossible to create a stringy object
 * from a null pointer and a nonzero length.
 */
            throw std::logic_error("Invalid ucs4_it object (invalid base, nonzero length).");
        }

        const auto c = *begin;

        if (c >= 0b10000000U && c < 0b11000000U) {
            // Not at a valid UTF-8 character boundary.
            throw std::runtime_error(INVALID_UTF8_MSG);
        }

        const size_t to_advance = (c < 0b10000000U) ? 1U :
                                  (c < 0b11100000U) ? 2U :
                                  (c < 0b11110000U) ? 3U :
                                                      4U;
        if (to_advance > size) {
            throw std::runtime_error(INVALID_UTF8_MSG);
        }

        *this = ucs4_it(begin + to_advance, size - to_advance);
        return *this;
    }

    constexpr ucs4_it operator++(int) {
        if (size == 0) {
            return *this;
        }

        ucs4_it to_return = *this;
        ++*this;
        return to_return;
    }

/* A Rust-style next() member function equivalent to *it++, which avoids
 * performing the same checks twice.
 */
    constexpr value_type next() {
/* The default ucs4_it object references an empty string, and can be
 * dereferenced.
 */
        if (size == 0)
          return 0;

        if (!begin) {
/* This is a logic error: it should be impossible to create a stringy object
 * from a null pointer and a nonzero length.
 */
            throw std::logic_error("Invalid ucs4_it object (invalid base, nonzero length).");
        }

/* A not-particularly-optimized implementation with error-checking
 * and low cyclomatic complexity.
 */
        const auto c1 = begin[0];
        if        (c1 < 0b10000000U) {
            begin += 1U;
            size -= 1U;
            return c1;
        } else if (c1 < 0b11000000U) {
            throw std::runtime_error(INVALID_UTF8_MSG);
        } else if (c1 < 0b11100000U && size >= 2U) {
            const auto c2 = begin[1];
            if (c2 < 0b10000000U || c2 >= 0b11000000U) {
                throw std::runtime_error(INVALID_UTF8_MSG);
            }
            begin += 2U;
            size -= 2U;
            return (c1 & 0b00011111U) << 6U |
                   (c2 & 0b00111111U);
        } else if (c1 < 0b11110000U && size >= 3U) {
            const auto c2 = begin[1];
            const auto c3 = begin[2];

            if (c2 < 0b10000000U || c2 >= 0b11000000U ||
                c3 < 0b10000000U || c3 >= 0b11000000U) {
                throw std::runtime_error(INVALID_UTF8_MSG);
            }

            begin += 3U;
            size -= 3U;
            return (c1 & 0b00001111U) << 12U |
                   (c2 & 0b00111111U) << 6U |
                   (c3 & 0b00111111U);
        } else if (c1 < 0b11111000U && size >= 4U) {
            const auto c2 = begin[1];
            const auto c3 = begin[2];
            const auto c4 = begin[3];

            if (c2 < 0b10000000U || c2 >= 0b11000000U ||
                c3 < 0b10000000U || c3 >= 0b11000000U ||
                c4 < 0b10000000U || c4 >= 0b11000000U )
                {
                throw std::runtime_error(INVALID_UTF8_MSG);
            }

            begin += 4U;
            size -= 4U;
            return (c1 & 0b00000111U) << 18UL | // On a 16-bit machine, unsigned int operands might overflow.
                   (c2 & 0b00111111U) << 12U |
                   (c3 & 0b00111111U) << 6U |
                   (c4 & 0b00111111U);
        } else {
            throw std::runtime_error(INVALID_UTF8_MSG);
        }
    }

    friend constexpr void ::ucs4::swap(ucs4_it&, ucs4_it&) noexcept;
    friend constexpr std::strong_ordering operator<=>( const ucs4_it& left,
                                                       const ucs4_it& right )
        noexcept;
    friend inline ucs4_it begin(const std::string_view source) noexcept;
    friend inline ucs4_it begin(const char* const source) noexcept;
    friend constexpr ucs4_it begin(const std::u8string_view source) noexcept;
    friend inline ucs4_it begin(const char8_t* const source) noexcept;
    friend inline ucs4_it end(const std::string_view source) noexcept;
    friend constexpr ucs4_it end(const std::u8string_view source) noexcept;
};

constexpr void swap(ucs4_it& left, ucs4_it& right) noexcept {
    std::swap(left.begin, right.begin);
    std::swap(left.size, right.size);
}

/* Leave it up to the programmer to compare only iterators that index the same
 * object, and let them shoot themselves in the foot.  For example, it is
 * valid to compare two iterators within different substrings of the same
 * string, with different start and end points.
 */
constexpr std::strong_ordering operator<=>( const ucs4_it& left,
                                            const ucs4_it& right) noexcept {
    return left.begin <=> right.begin;
}

constexpr bool operator==( const ucs4_it left,
                           const ucs4_it right ) noexcept {
    return (left <=> right) == std::strong_ordering::equal;
}

/* Because we declare the comparison operators as non-member overloads, we
 * could also provide overloads to compare a ucs4_it and a char* or char8_t*.
 */

 inline ucs4_it begin(const std::string_view source) noexcept {
    return ucs4_it(reinterpret_cast<const char8_t*>(source.data()), source.size());
 }

inline ucs4_it begin(const char* const source) noexcept {
    return ucs4_it(reinterpret_cast<const char8_t*>(source), std::strlen(source)+1U);
}

constexpr ucs4_it begin(const std::u8string_view source) noexcept {
    return ucs4_it(source.data(), source.size());
}

inline ucs4_it begin(const char8_t* const source) noexcept {
    return ucs4_it(source, std::strlen(reinterpret_cast<const char*>(source)) + 1U);
}

inline ucs4_it end(const std::string_view source) noexcept {
    return ucs4_it(reinterpret_cast<const char8_t*>(source.data()) + source.size(), 0);
}

constexpr ucs4_it end(const std::u8string_view source) noexcept {
    return ucs4_it(source.data() + source.size(), 0);
}

} // end namespace ucs4

Some test boilerplate:

#include <concepts>
#include <cstdlib>
#include <iostream>
#include <source_location>
#include <string>
#include <string_view>

using std::cerr, std::cout, std::exit;
using namespace std::literals::string_view_literals;

template<class T, class U>
    requires (std::equality_comparable_with<T, U>)
constexpr void expect_test(const T& got,
                           const U& expected,
                           const std::source_location location =
    std::source_location::current()) {
    if (got != expected) {
        cout.flush();
        cerr << "Test in " << location.function_name()
             << " (" << location.file_name()
             << ':' << location.line()
             << ':' << location.column()
             << ") failed!\n";
        exit(EXIT_FAILURE);
    }

    cout << "Test in " << location.function_name()
         << " (" << location.file_name()
         << ':' << location.line()
         << ':' << location.column()
         << ") passed.\n";
}

And a simple test driver:

static_assert(std::forward_iterator<ucs4::ucs4_it>);
static_assert(!(ucs4::ucs4_it() != ucs4::ucs4_it()));
static_assert(ucs4::ucs4_it() <= ucs4::ucs4_it());
static_assert(ucs4::ucs4_it() >= ucs4::ucs4_it());
static_assert(!ucs4::ucs4_it());

int main() {
    expect_test(*ucs4::begin("!"sv), U'!');
    expect_test(*ucs4::begin(u8"¿"sv), U'¿');
    expect_test(*ucs4::begin(u8"א"sv), U'א');
    expect_test(*ucs4::begin(u8"𝓐"sv), U'𝓐');

    {
        constexpr auto test_sv = u8"☪☮∈✡℩☯✝ \U0001F644"sv;
        constexpr auto expected = U"☪☮∈✡℩☯✝ \U0001F644"sv;
        const std::u32string test1(ucs4::begin(test_sv), ucs4::end(test_sv));
        expect_test(test1, expected);

        std::u32string test2;
        for(auto it = ucs4::begin(test_sv); it; ++it) {
            test2.push_back(*it);
        }
        expect_test(test2, expected);

        std::u32string test3;
        auto it = ucs4::begin(test_sv);
        while(it) {
            test3.push_back(it.next());
        }
        expect_test(test3, expected);
    }
    return EXIT_SUCCESS;
}

Code on Godbolt Compiler Explorer.

\$\endgroup\$
0
\$\begingroup\$

Here is my own version, a bit faster and correcting a bug in the original version (3-bytes/4-bytes decoding are not properly done). Only one parameter that will receive the next available character pointer in the string. This is a non-validating method that expect a valid UTF8 string as input. Was done for C++ v17. With v20, char and uint8_t would be char8_t, without using the reinterpret_cast stuff.:

#include <cwchar>
#include <iostream>    
    
// Non-validating algorithm
char32_t toChar32(const char **str) {
    const uint8_t *s = reinterpret_cast<const uint8_t *>(*str);
    uint8_t c1 = *s++;

    char32_t res;

    if (c1 >= 0xC0) {
        uint8_t c2 = *s++;

        if (c1 >= 0xE0) {
            uint8_t c3 = *s++;

            if (c1 >= 0xF0) {
                uint8_t c4 = *s++;

                res = ((c1 & 0b00000111) << 18) | ((c2 & 0b00111111) << 12) |
                        ((c3 & 0b00111111) << 6) | (c4 & 0b00111111);
            } else {
                res = ((c1 & 0b00001111) << 12) | ((c2 & 0b00111111) << 6) | (c3 & 0b00111111);
            }
        } else {
            res = ((c1 & 0b00011111) << 6) | (c2 & 0b00111111);
        }

    } else {
        res = c1 & 0b01111111;
    }

    *str = reinterpret_cast<const char *>(s);
    return res;
}

int main() {
    const char *case1 = "\x24";
    const char *case2 = "\xC2\xA3";
    const char *case3 = "\xE0\xA4\xB9";
    const char *case4 = "\xF0\x90\x8D\x88";

    const char32_t res1 = (char32_t)0x24;
    const char32_t res2 = (char32_t)0xA3;
    const char32_t res3 = (char32_t)0x939;
    const char32_t res4 = (char32_t)0x10348;

    const char *s = case1;
    if (toChar32(&s) != res1) std::cout << "Case 1 is wrong!" << std::endl;

    s = case2;
    if (toChar32(&s) != res2) std::cout << "Case 2 is wrong!" << std::endl;
    
    s = case3;
    if (toChar32(&s) != res3) std::cout << "Case 3 is wrong!" << std::endl;
    
    s = case4;
    if (toChar32(&s) != res4) std::cout << "Case 4 is wrong!" << std::endl;
    
    std::cout << "End" << std::endl;
}
\$\endgroup\$
3
  • 3
    \$\begingroup\$ "Answers that merely provide an alternate solution with no explanation or justification do not constitute valid Code Review answers and may be deleted. In addition to criticisms, pointing out good practices in the code is also a form of helpful feedback." ([link])(codereview.stackexchange.com/help/how-to-answer) \$\endgroup\$ Sep 9, 2023 at 13:24
  • \$\begingroup\$ @BillalBegueradj Poster made the observation that the code could be faster. \$\endgroup\$
    – pacmaninbw
    Sep 9, 2023 at 17:18
  • \$\begingroup\$ As indicated in my comment, the code correct a bug in the original version… \$\endgroup\$
    – Guy
    Sep 10, 2023 at 14:19

Your Answer

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

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