Skip to content

← Values, Types, Initialisation step 3 of 4

Easy Primitives

Scale pixel channels safely

Braced initialization can expose a conversion that would otherwise discard part of a value:

int a(3.9);   // becomes 3; a compiler may warn
int b{3.9};   // ill-formed: narrowing conversion

Which conversions do braces reject?

In C++20, list-initialization rejects floating-point to integer conversions, even when the number has no fractional part. Some other conversions have exceptions for constant expressions whose values fit exactly:

int a{3.0};                 // error: double to int
int b{3};                   // OK
std::uint8_t c{300};        // error: outside 0..255
std::uint8_t d{65};         // OK: constant fits
double e{3};                // OK: constant is represented exactly

Include <cstdint> for std::uint8_t, the unsigned 8-bit type available in this environment. An integer variable converted to double can still be a narrowing conversion under these rules; the constant example above is not a general exemption for integers. Wider-to-smaller floating-point conversions also have specific constant-expression exceptions.

Braces do not check every calculation for you. An explicit cast can perform a conversion inside braces, so its safety remains your responsibility.

Braces can select a different constructor

For a vector, these declarations mean different things:

std::vector<int> counts(10);  // ten elements, all zero
std::vector<int> values{10};  // one element, whose value is ten

Include <vector> for this example. The braced form selects the initializer-list constructor. Choose the form that expresses the constructor you intend to call.

Empty braces are also useful for initializing scalar values:

int x{};       // zero
double y{};    // 0.0
int* p{};      // null pointer

By contrast, a local int x; without an initializer has an indeterminate value; reading it in an ordinary expression is undefined behavior in C++20. Class and aggregate initialization follow their own rules, so {} is not a universal promise to zero every member of every class.

Your task

Implement:

Pixel scale(const Pixel& p, double factor);

Pixel has three std::uint8_t channels named r, g and b. For each channel:

  1. Multiply its value by factor using double arithmetic.
  2. Round to the nearest integer, with halfway values rounded away from zero.
  3. Clamp to the inclusive range 0..255 while the value is still a double.
  4. Convert the bounded value to std::uint8_t.

Return a new Pixel and leave p unchanged. Inputs use channels in 0..255 and finite factors whose channel products remain finite. Zero or negative factors produce zero in every channel.

For example, scaling [5, 100, 200] by 1.5 produces [8, 150, 255]: 7.5 rounds to 8, 150 stays unchanged, and 300 is clamped to 255. The harness returns the channels in [r, g, b] order.

The starter uses explicit functional casts but does not round or clamp. Floating-point to integer conversion truncates toward zero. If that truncated value is outside the integer type's range, behavior is undefined, even for an unsigned destination. Do not rely on wraparound or clamp after converting.

Use an explicit static_cast only after establishing the valid range. It does not perform a range check itself. Construct the result with C++20 designated initializers in member declaration order:

Pixel{.r = red, .g = green, .b = blue}

Naming the fields makes their mapping easier to review, but cannot prevent assigning the wrong expression to a named field.