c++arraysargumentscimg

How to pass a constant array as argument to a C++ function/method


Problem:
I have a C++11 method (CImg class) that takes in input an unsigned char[3] array to specify the color. I'm compiling with a mingw compiler and -std=gnu++11 option.
For reference this is the method I need to call:

template<typename tc>
CImg<T>& draw_line(int x0, int y0, int x1, int y1, const tc *const color, const float opacity=1, const unsigned int pattern=~0U, const bool init_hatch=true);

If I try to construct the array locally I get the following error. The array is created and destroyed before being passed, making the argument invalid when the function is called.

image.draw_line( 20, 90, 190, 10, (unsigned char[3]){0,255,0}, 0.9 );
"ERROR: taking address of temporary array"

Attempt 2: Local array declared before function call using local var
Researching the issue, the most common solution is to create an explicit local var. It clearly works since it's destroyed later, when it goes out of scope. It obviously works, but I really feel there should be a more readable and less verbose way of doing it.

unsigned char my_temp_color[3] = {0,255,0};
image.draw_line( 20, 90, 190, 10, my_temp_color, 0.9 );

Attempt 3: Constant local array
I fooled around with different ways to pass the array. I tried with structures, etc... My mingw compiler is happy with passing a local constant array and does not give any warnings or errors.

image.draw_line( 20, 90, 190, 10, (const unsigned char[3]){0,255,0}, 0.9 );

Proposed Solution 1: std::array
If I try passing a naked std::array, the compiler cannot resolve the type to unsigned char[3]

image.draw_line( 20, 90, 190, 10, std::array<unsigned char,3>{ {0,255,0} }, 0.9 );
"error: no matching function for call to 'cimg_library::CImg<float>::draw_line(int, int, int, int, std::array<unsigned char, 3>, double)"

It works if I pass a local std::array using .data() method to get the pointer, but it is even more verbose than a regular array.

std::array<unsigned char,3> my_temp_color = { {0,255,0} };
image.draw_line( 20, 90, 190, 10, my_temp_color.data(), 0.9 );

It works if I do it all inline. With this solution I'm not sure on where the object is created/destroyed and I'm worried about the pointer becoming invalid and passing trash data silently.

image.draw_line( 20, 90, 190, 10, std::array<unsigned char,3>{ {0,255,0} }.data(), 0.9 );

Proposed Solution 2: std::string
The initialization is harder to read with std::string. If I try passing a naked std::string, the compiler cannot resolve the type to unsigned char[3]

image.draw_line( 20, 90, 190, 10, std::string("/0/255/0"), 0.9 );
"no matching function for call to 'cimg_library::CImg<float>::draw_line(int, int, int, int, std::__cxx11::string, double)'"

Like std::array it works if I i use .data() method, but I have the same worries.

Proposed Solution 3: wrapper + structures
The third solution proposed by @jarod42 is to create wrappers around the third party library and use a more convenient and readable interface. Being able to change the interface allows to use structures that solve all problems of scoping and temporary values. It also has the added benefit of adding an abstraction layer and making future changes easier. I decided to incapsulate wrappers and structures as static methods inside a class. I really like this solution.

The using statement can be used to make the code less verbose. My compiler can also automatically deduce the type from just the brackets. For the scope of this question I left it extended to understand what's going on.

//Build option:
//-std=gnu++11

//STD
#include <string>
//CImg
#define cimg_use_png
#define cimg_display 0
#include "CImg.h"
using namespace cimg_library;
//PNG library
#include "png.h"

//CImg wrapper. Add an abstraction layer to CImg to use less verbose Point and Color structures
class CImg_wrapper
{
    public:
        //2D Point
        typedef struct _Point_2d
        {
            int g_s32_x, g_s32_y;
        } Point_2d;
        //3 channel color
        typedef struct _Color_3u8
        {
            unsigned char g_u8p_rgb[3];
        } Color_3u8;
        //Draw text on an image
        template<typename T>
        static void draw_text(CImg<T>& image, Point_2d origin, std::string text, Color_3u8 foreground_color, float opacity, int font_size )
        {
            image.draw_text(origin.g_s32_x, origin.g_s32_y, text.c_str(), foreground_color.g_u8p_rgb, 0, opacity, font_size);
            return;
        }
        //Draw a line on an image
        template<typename T>
        static void draw_line(CImg<T>& image, Point_2d p1, Point_2d p2, Color_3u8 color, float transparency)
        {
            image.draw_line(p1.g_s32_x, p1.g_s32_y, p2.g_s32_x, p2.g_s32_y, color.g_u8p_rgb, transparency);
            return;
        }
};  //CImg_wrapper

//DEMO
int main(int argc, char** argv)
{
    //Create image
    CImg<float> image
    (
        //width
        200,
        //height
        100,
        //Depth. 1 for a 2D image
        1,
        //Number of channels
        3
    );
    //draw text on the image
    CImg_wrapper::draw_text(image, (CImg_wrapper::Point_2d){20, 10}, std::string("Shaka"), (CImg_wrapper::Color_3u8){0,0,255}, 0.9f, 24 );
    //draw a line on the image
    CImg_wrapper::draw_line(image, (CImg_wrapper::Point_2d){20, 90}, (CImg_wrapper::Point_2d){190, 10}, (CImg_wrapper::Color_3u8){0,255,0}, 0.9f );
    //The compiler is able to deduce the type from just the brackets if needed
    //CImg_wrapper::draw_line(image, {20, 90}, {190, 10}, {0,255,0}, 0.9f );
    //Save image on file
    image.save("file.png");

    return 0;
}

output:
Output PNG image

source code:
https://github.com/OrsoEric/2021-02-02-CImg-PNG-Test

Questions:

  1. Is passing (const unsigned char[3]){0,255,0} C++11 compliant or it's just a quirk of the mingw compiler that makes it work?
  2. Is there another way I have not considered to declare and pass the array as argument?
  3. With the following calls are the temporary objects destroyed after the statement (safe), or before the call (causing the pointer to potentially become invalid)?
image.draw_line( 20, 90, 190, 10, std::array<unsigned char,3>{ {0,255,0} }.data(), 0.9 );
image.draw_line( 20, 90, 190, 10, std::string("/0/255/0").data(), 0.9 );

Solution

  • When working with 3rd library, it is generally good to write wrapper around them.

    struct Point
    {
       int x = 0;
       int y = 0;
    }
    
    struct Color
    {
        Color(unsigned char r, unsigned char g, unsigned char b, unsigned char a = 0) :
            rgba{r, g, b, a}
        {}
    
        // ...
        unsigned char rgba[4]{};
    };
    
    template<typename T>
    void draw_line(CImg<T>& img, Point p1, Point p2, Color color)
    {
        img.draw_line(p1.x, p1.y, p2.x, p2.y, color.rgba);
    }
    

    And so you might use:

    draw_line(img, Point{20, 90}, Point{190, 10}, Color{0,255,0});
    draw_line(img, {20, 90}, {190, 10}, {0,255,0});
    

    Note: You might even hide CImg<T> in your own class/interface.