Должен ли я использовать() или {} при пересылке аргументов?

У меня есть следующий класс:

struct foo
{
    std::size_t _size;
    int* data;
public:
    explicit foo(std::size_t s) : _size(s) { }
    foo(std::size_t s, int v)
        : _size(s)
    {
        data = new int[_size];
        std::fill(&data[0], &data[0] + _size, v);
    }

    foo(std::initializer_list<int> d)
        : _size(d.size())
    {
        data = new int[_size];
        std::copy(d.begin(), d.end(), &data[0]);
    }

    ~foo() { delete[] data; }

    std::size_t size() const { return _size; }
};

И я хочу передать ему аргументы следующим образом:

template <typename... Args>
auto forward_args(Args&&... args)
{
    return foo{std::forward<Args>(args)...}.size();
    //--------^---------------------------^
}

std::cout << forward_args(1, 2) << " " << forward_args(1) << " " 
          << forward_args(2) << "\n";

Если я заменю {} на (), вывод будет 1 1 2 вместо 2 1 1.

Что будет иметь смысл для моего класса?

Ответы

Ответ 1

Использование {} vs. () определяет, какой конструктор вызывается.

  • {} вызовет форму foo(std::initializer_list<int> d), и вы получите ожидаемые результаты.

  • () вызовет explicit foo(std::size_t s) и foo(std::size_t s, int v), и во всех случаях первый элемент будет размером, поэтому, учитывая аргументы, вы получите результаты, которые вы видите.

Какую форму в пользу зависит от того, какую семантику вы хотите использовать для поддержки метода forward_args. Если вы хотите передать параметры "как есть", тогда следует использовать () (в этом случае пользователю необходимо указать initialiser_list в качестве аргумента для начала).

Возможно (возможно) форма , которую вы хотите одобрить, () и используется следующим образом;

template <typename... Args>
auto forward_args(Args&&... args)
{
    return foo(std::forward<Args>(args)...).size();
    //--------^---------------------------^
}

int main()
{
    std::cout << forward_args(std::initializer_list<int>{1, 2}) << " "
              << forward_args(std::initializer_list<int>{3}) << " " 
              << forward_args(std::initializer_list<int>{4}) << "\n";
}

Боковое примечание; страница cppreference на initializer_list имеет хороший пример этого поведения.

Если требуется (т.е. поддерживать forward_args({1,2})), , вы можете обеспечить перегрузку для initializer_list;

template <class Arg>
auto forward_args(std::initializer_list<Arg> arg)
{
    return foo(std::move(arg)).size();
}

Как отмечено: с учетом initializer_list конструкторы классов в конечном счете являются источником путаницы, и это проявляется во время построения объекта. Существует разница между foo(1,2) и foo{1,2}; последний вызывает форму initializer_list конструктора. Один из способов решения этой проблемы - использовать "тег" для дифференциации "нормальных" форм конструктора из формы initializer_list.

Ответ 2

В этом случае, поскольку класс имеет конструктор, который принимает std::initializer_list, использование {} в функции factory часто изменяет семантическое значение любого списка аргументов, через которое вы проходите, путем превращения любой комбинации аргументов длиннее 2 в список initializer_list.

Это будет удивительно для пользователей этой функции.

Поэтому используйте форму (). Пользователи, которые хотят передать файл initializer_list, могут так явно, вызывая

forward_args({ ... });

NB для приведенного выше синтаксиса для работы вам нужно будет предоставить дополнительную перегрузку:

template <class T>
auto forward_args(std::initializer_list<T> li)
{
    return foo(li).size();
}

Ответ 3

Действительно, вы не должны определять свой класс foo как это (при условии, что он находится под вашим контролем). Два конструктора имеют перекрытие и приводят к плохой ситуации, когда две инициализации:

foo f1(3);
foo f2{3};

Значит две разные вещи; и люди, которым необходимо написать функции пересылки, сталкиваются с неразрешимыми проблемами. Это подробно описано в этом сообщении в блоге. Также std::vector страдает от одной и той же проблемы.

Вместо этого я предлагаю использовать конструктор с тегами:

struct with_size {}; // just a tag

struct foo
{
    explicit foo(with_size, std::size_t s);

    explicit foo(with_size, std::size_t s, int v);

    foo(std::initializer_list<int> d);

    // ...
}

Теперь у вас нет перекрытия, и вы можете безопасно использовать вариант {}.