It’s great that you provide operator overloads, but it’s also nice to have names

Raymond Chen

Operator overloading.

Looks great. Reduces verbosity.

Until it doesn’t.

Consider this overloaded function call operator:

struct StorageLoader
{
    template<typename DataType>
    DataType operator()(StorageOptions<DataType> const* options);
};

The idea is that you can use the function call operator on a Storage­Loader object to load data from storage, using a StorageOptions to describe how you want it to be loaded.

StorageOptions<Data1> data1Options;
data1Options.ignore_missing(true);

StorageLoader storageLoader;
Data1 data1 = storageLoader(&data1Options);

The parameter is accepted as a pointer so you can pass nullptr to indicate that you accept all defaults.

// Oops, this doesn't work.
Data1 data1 = storageLoader(nullptr);

The nullptr doesn’t work because the compiler can’t read your mind to figure out which overload you’re trying to call. You have to help it along, either by refining the type of the parameter:

Data1 data1 = storageLoader(static_cast<StorageOptions<Data1>*>(nullptr));

or by explicitly specializing the function call operator.

Data1 data1 = storageLoader.operator()<Data1>(nullptr);

Neither of these is very attractive, and they certainly defeat any conciseness benefit of an overloaded operator.

I personally am a fan of giving named function equivalents to overloaded operators, particularly if they are templated. In this case, I would have done something like

struct StorageLoader
{
    template<typename DataType>
    DataType Load(StorageOptions<DataType> const* options);

    template<typename DataType>
    DataType operator()(StorageOptions<DataType> const* options)
    { return Load(options); }
};

The function call operator is just a convenient shorthand for calling the Load method.

// Using function call operator
data1 = storageLoader(&data1Options);

// Using named method
data1 = storageLoader.Load(&data1Options);

// Named method works better for nullptr
data1 = storageLoader.Load<Data1>(nullptr);

And then I can make the parameter to Load default to nullptr:

struct StorageLoader
{
    template<typename DataType>
    DataType Load(StorageOptions<DataType> const* options
                  = nullptr);

    ...
};

which allows you to write

// If no parameters, then use default options
data1 = storageLoader.Load<Data1>();

Sometimes, the meaning of an overloaded operator is unclear, in which case having an explicit name also helps avoid confusion over what it does. (I’m looking at you, overloaded address-of operator.)

Also, giving a name to the overloaded operator makes generating a pointer-to-method a little less awkward.

8 comments

Discussion is closed. Login to edit/delete existing comments.

  • Marc Fauser 0

    Another great article. 👍

    I have one question about an old article:
    https://devblogs.microsoft.com/oldnewthing/20140908-00/?p=53
    Here you start notepad and put the text into it.
    Since Notepad is a store app and has tabs, this doesn’t work anymore.
    WaitForInputIdle will wait forever. The process only has 0 for MainWindowHandle as the exe is only a wrapper to start the store app and notepad is only a single process with many windows and tabs and the process I start closes very soon.
    Could you should how you do this with the current notepad? My approach is not really working. Waiting 100ms after starting notepad, searching all processes for notepad and enumerate over all windows until I find “Untitled – Notepad”. If I don’t wait enough, the window title is only ” – Notepad”.
    Still I cannot find the edit control with UI Automation to set the text.
    I’ve tried to search Win32_Process for parent processes but this doesn’t work if Notepad was already started.
    It would be very interesting how you can solve this problem.

    • Daniel Roskams 0

      jeez does it take over 100ms for notepad to start up these days? this is the power of modern apps.

      • Marc Fauser 0

        Store apps are always slow to start.
        The whole concept of the store apps is not very good.
        It’s like a foreign body in Windows.

  • Kythyria Tieran 0

    Rust solves this one straightforwardly: each overloadable operator has a corresponding trait that is normal aside from the compiler knowing about the correspondence. To overload the operator, you implement the trait.

    And then there’a a general syntax for referring to any function, so you can get a pointer to it, or call it, that way (this breaks a bit for anonymous types, but is easily worked around).

  • Risto Lankinen 1

    Why not modify the StorageLoader a bit:

    struct StorageLoader
    {
        template
        DataType operator()(StorageOptions const* options = nullptr);
    };
    

    Then your “accept all defaults” function call would look even nicer:

    Data1 data1 = storageLoader();
    

    Cheers!

    • Bas Mommenhof 0

      Math teacher: “I want to teach you guys a new technique called: Multiplication.”
      Johnny: “But we can do that using Addition.”

      Math teacher: “Yes, but I am trying to teach you Multiplication”
      Johnny: “Addition !!!”

      Math teacher: “Multiplication is just another way of doing things, with the added advantage that it can provide clarity.”
      Johnny: “Addition !!!”

      Math teacher: *sigh*

    • Raymond ChenMicrosoft employee 0

      You cannot deduce the return type. What would auto data = storageLoader() use for the DataType?

  • Alex “Kappa” Kapranoff 1

    “The parameter is accepted as a pointer so you can pass nullptr to indicate that you accept all defaults.” seems like a non-ideal API design that also leads to the described problem.
    An example of a slightly better API, I think, would require an explicit StorageOptions constant to indicate default options. It will also solve the problem with the operator().

Feedback usabilla icon