std::setprecision sets the number of significant figures. How do I use iomanip to set the precision?

Viewed 506

I have always found iomanip confusing and counter intuitive. I need help.

A quick internet search finds (https://www.vedantu.com/maths/precision) "We thus consider precision as the maximum number of significant digits after the decimal point in a decimal number" (the emphasis is mine). That matches my understanding too. However I wrote a test program and:

stm << std::setprecision(3) << 5.12345678;
std::cout << "5.12345678: " << stm.str() << std::endl;
stm.str("");

stm << std::setprecision(3) << 25.12345678;
std::cout << "25.12345678: " << stm.str() << std::endl;
stm.str("");

stm << std::setprecision(3) << 5.1;
std::cout << "5.1: " << stm.str() << std::endl;
stm.str("");

outputs:

5.12345678: 5.12
25.12345678: 25.1
5.1: 5.1

If the precision is 3 then the output should be:

5.12345678: 5.123
25.12345678: 25.123
5.1: 5.1

Clearly the C++ standard has a different interpretation of the meaning of "precision" as relates to floating point numbers.

If I do:

stm.setf(std::ios::fixed, std::ios::floatfield);

then the first two values are formatted correctly, but the last comes out as 5.100.

How do I set the precision without padding?

3 Answers

I don't think you can do it nicely. There are two candidate formats: defaultfloat and fixed. For the former, "precision" is the maximum number of digits, where both sides of the decimal separator count. For the latter "precision" is the exact number of digits after the decimal separator.

So your solution, I think, is to use fixed format and then manually clear trailing zeros:

#include <iostream>
#include <iomanip>
#include <sstream>

void print(const double number)
{
    std::ostringstream stream;
    stream << std::fixed << std::setprecision(3) << number;
    auto string=stream.str();
    while(string.back()=='0')
        string.pop_back();
    if(string.back()=='.') // in case number is integral; beware of localization issues
        string.pop_back();
    std::cout << string << "\n";
}

int main()
{
    print(5.12345678);
    print(25.12345678);
    print(5.1);
}

You can try using this workaround:

decltype(std::setprecision(1)) setp(double number, int p) {
    int e = static_cast<int>(std::abs(number));
    e = e != 0? static_cast<int>(std::log10(e)) + 1 + p : p;
    while(number != 0.0 && static_cast<int>(number*=10) == 0 && e > 1) 
        e--; // for numbers like 0.001: those zeros are not treated as digits by setprecision.
    return std::setprecision(e);
}

And then:

auto v = 5.12345678;
stm << setp(v, 3) << v;

Another more verbose and elegant solution is to create a struct like this:

struct __setp {
    double number;
    bool fixed = false;
    int prec;
};

std::ostream& operator<<(std::ostream& os, const __setp& obj)
{
    if(obj.fixed)
        os << std::fixed;
    else os << std::defaultfloat;
    os.precision(obj.prec);
    os << obj.number; // comment this if you do not want to print immediately the number
    return os;
}

__setp setp(double number, int p) {
     __setp setter;
     setter.number = number;
     
    int e = static_cast<int>(std::abs(number));
    e = e != 0? static_cast<int>(std::log10(e)) + 1 + p : p;
    while(number != 0.0 && static_cast<int>(number*=10) == 0)
        e--; // for numbers like 0.001: those zeros are not treated as digits by setprecision.

    if(e <= 0) {
        setter.fixed = true;
        setter.prec = 1;
    } else
        setter.prec = e;
    return setter;
}

Using it like this:

auto v = 5.12345678;
stm << setp(v, 3);

The fixed format gives almost what you want except that it preserves trailing zeros. There is no built-in way to avoid that but you can easily remove those zeros manually. For example, in C++20 you can do the following using std::format:

std::string format_fixed(double d) {
  auto s = fmt::format("{:.3f}", d);
  auto end = s.find_last_not_of('0');
  return end != std::string::npos ? std::string(s.c_str(), end + 1) : s;
}

std::cout << "5.12345678: " << format_fixed(5.12345678) << "\n";
std::cout << "25.12345678: " << format_fixed(25.12345678) << "\n";
std::cout << "5.1: " << format_fixed(5.1) << "\n";

Output:

5.12345678: 5.123
25.12345678: 25.123
5.1: 5.1

The same example with the {fmt} library, std::format is based on: godbolt.

Disclaimer: I'm the author of {fmt} and C++20 std::format.

Related