Home > database >  Replace C preprocessor macro with something that can initialize a struct
Replace C preprocessor macro with something that can initialize a struct

Time:11-24

I'm working with WinAPI's CreateDialogIndirect function, which has some requirements on the DLGTEMPLATE and DLGTEMPLATEEX structures pointed to by the second parameter. My code works well, however, I would like to get rid of the #define macros.

I created a simplified example to focus on the macros. This is a working program with macros, it can be compiled, and outputs what is expected:

#include <iostream>

int wmain()
{
#define TITLE L"Title"
    struct {
        wchar_t title[ sizeof( TITLE ) / sizeof( TITLE[ 0 ] ) ];
        int font_size;
    } s = {
        TITLE,
        12,
    };

    std::wcout
        << L"s.title = "     << s.title     << std::endl
        << L"s.font_size = " << s.font_size << std::endl;

    return 0;
}

In Visual Studio 2022, I see three dots at the #define macro, and I can read the following tooltip:

  • Macro can be converted to constexpr
  • Show potential fixes
  • Convert macro to constexpr

I would like to see it converted to something to avoid macros, so I click on it, and the code is changed to this:

#include <iostream>

int wmain()
{
constexpr auto TITLE = L"Title";
    struct {
        wchar_t title[ sizeof( TITLE ) / sizeof( TITLE[ 0 ] ) ];
        int font_size;
    } s = {
        TITLE,
        12,
    };

    std::wcout
        << L"s.title = "     << s.title     << std::endl
        << L"s.font_size = " << s.font_size << std::endl;

    return 0;
}

If I hit F7 to compile, I get the following error message:

  • example.cpp(10,9): error C2440: 'initializing': cannot convert from 'const wchar_t *const ' to 'wchar_t'

I would not like to enter L"Title" two times, and I would not like to calculate the length of the string manually. So, what can be a good substitute for the macro, which is capable of both initializing the struct and determining the size of the array in the struct?

CodePudding user response:

Depends what you really want. One way is to use std::wstring_view instead of the array, but this may suit your needs or not. If it does, it's pretty easy:

    struct {
        std::wstring_view title = L"Title";
        int font_size{12};
    } s;

CodePudding user response:

DLGTEMPLATEEX is a special case. In fact, there is no structures with such a name in the wndows headers. If you want to use a structure, you must create you own, with custom array sizes.4

Example

constexpr TCHAR TITLE[] = _T("Title"); // IMPORTANT! To be 100% sure that's a array,
                                       // declare it explicitely.

struct MyDlgTemplateEx {
  WORD      dlgVer;
  WORD      signature;
  DWORD     helpID;
  DWORD     exStyle;
  DWORD     style;
  WORD      cDlgItems;
  short     x;
  short     y;
  short     cx;
  short     cy;
  TCHAR     menu[1];
  TCHAR     WwindowClass[1];
  TCHAR     title[sizeof(TITLE) / sizeof(TITLE[0]);
  WORD      pointsize;
  WORD      weight;
  BYTE      italic;
  BYTE      charset;
  TCHAR     typeface[1];  // a NULL string.
};

The first thing that's obvious it that there are quite a few member variables. Doing unnamed in-place initialization (as in your example), with no explicit member names can very easily lead to mistakes and bugs.

The most common way of initializing such large structures is explicitely:

// ...


MyDlgTemplateEx tmpl = {}; // this declatres and fills structure with zeroes.

// initialize all non-zero members explicitely.
// cutting and pasting the structure declaration and editing around that is fairly fast. 
tmpl.dlgVer = 1;
tmpl.signature = 0xFFFF;   // this is a DLGTEMPLATEEX struct.
tmpl.exStyle = WS_EX_DLGMODALFRAME;
tmpl.style = WS_CAPTION;
tmpl.cDlgItems = 1;
tmpl.x = 10;
tmpl.y = 10;
tmpl.cx = 100;
tmpl.cy = 100;
_tcscpy_s(tmpl.title, TITLE);  // must copy into array that's in structure.

CodePudding user response:

A template is a reasonably common replacement for macro usage. Since you don't want to manually calculate the length of the string (I don't blame you), let's make a length (the array length, not the string length) the template parameter. Your anonymous struct becomes a likely candidate for being the templated entity, as you do end up with a different type each time you change the length of your title.

The actual conversion to a template is simple enough (given the level of the question) that I won't go over the details here. The nifty part comes when you write a constructor. By using the template parameter in the argument list, the compiler will be able to deduce it. This gives you the ease-of-use you appear to be looking for, albeit at the cost of some additional setup.

Caveat: Deducing the template parameter via an argument to the constructor is a C 17 feature. Anyone stuck on C 11 or 14 (hopefully not many of you) can get a similar result by writing a separate function template that constructs and returns a struct. I'll leave that as an exercise.

#include <iostream>
#include <cstring>    // For memcpy


template <size_t N>
struct DlgTemplate {
    wchar_t title[N];
    int font_size;

    // Constructor that will deduce `N`.
    // For those not familiar with this syntax: the incoming title_ is a
    // reference to an array with exactly N elements.
    DlgTemplate(const wchar_t (&title_)[N], int font_size_) :
        font_size(font_size_)
    {
        // Copy the title.
        std::memcpy(title, title_, sizeof(title));
    }

    // Reminder: N is the array length, which includes the null terminator.
    //           The string length is N-1.
};


int main()
{
    // As of C  17, the template argument can be deduced here.
    auto s = DlgTemplate(L"Title", 12);

    std::wcout
        << L"s.title = "     << s.title     << std::endl
        << L"s.font_size = " << s.font_size << std::endl;
}

The next consideration is probably how to deal with all the fields that were mercifully left out of the question. One option is to add more parameters to the constructor to handle all of the fields. However, in the interest of readability, I might be inclined to remove font_size_ as a constructor parameter (remembering to mark the constructor explicit) then set each field after construction. The following is what I have in mind for the initialization.

    auto s = DlgTemplate(L"Title");
    s.font_size = 12;
    //s.other_field = value;
    // etc.

There is one important detail to note from the question. The "potential fix" implemented by Visual Studio caused the length information to be lost.

constexpr auto TITLE = L"Title";

defines TITLE to be a pointer (to const wchar_t). Its size is fixed, independent of the length of the title. The calculation sizeof( TITLE ) / sizeof( TITLE[ 0 ] ) does not give the length of the title, and this TITLE will not work with the template.

constexpr wchar_t TITLE[] = L"Title";

defines TITLE to be an array, with the length information kept as part of the type. This TITLE can be used with the template.

  •  Tags:  
  • c
  • Related