June 5th, 2023

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

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.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

8 comments

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

  • Alex “Kappa” Kapranoff

    “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().

  • Risto Lankinen

    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!

    • Raymond ChenMicrosoft employee Author

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

    • Bas Mommenhof

      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*

  • Kythyria Tieran

    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).

  • Marc Fauser

    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...

    Read more
    • Daniel Roskams

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

      • Marc Fauser

        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.