LINUX.ORG.RU

Variadic templates и list initialization

 


0

4

Собственно, код (https://gcc.godbolt.org/z/TWW7M9z48):

#include <vector>
#include <string>

struct Foo
{
 std::string s;
 size_t b,e;
};

template<class... T>
auto Baz(struct Foo&& foo, T&&... t)
{
 return std::vector<struct Foo>({std::move(foo),std::move(t)...});
}

int main()
{
 //Baz({"q",10,20},{"g",11,50});
 Baz({"q",10,20});
 return 0;
}

Закоментированная строка не работает, компилятор ругается на невозможность вывода T. Звучит логично, но хотелось бы иметь возможность передавать структуры в функцию именно в виде списка инициализации. Как вариант, сработало бы

auto Baz(std::vector<struct Foo>&& foo);
Baz({{"q",10,20},{"g",11,50}});

но неохота ещё пару скобок писать.


Ответ на: комментарий от rumgot

move там потому, что структура Foo определена как приватная в классе, и объекты этого типа снаружи класса создавать нельзя, внутри Baz не нужна. Отсюда и заморочки эти с list initialization. Вообще, задача состоит в том, чтобы передать в функцию произвольное число групп из от одного до трёх параметров, недостающие параметры принимают значения по умолчанию. Если б не эти умолчания, можно было бы просто написать шаблон

Baz(const std:string& s, size_t b, size_t e, T&&... t)

и не париться.

JaM ()
Ответ на: комментарий от JaM

И что мешает тебе сделать так (это я про использование forward()):

  template <class... T>
  auto Baz(struct Foo&& foo, T&&... t) const {
    return PrivateBaz(std::vector<struct Foo>(
        {std::forward<Foo>(foo), std::forward<T>(t)...}));
  }
rumgot ★★★★★ ()
Последнее исправление: rumgot (всего исправлений: 2)
Ответ на: комментарий от JaM

Если у тебя есть публичный интерфейс в виде функции:

template<class... T>
auto Baz(struct Foo&& foo, T&&... t)

То делать объявление Foo (которое является частью этого публичного интерфейса) приватным - это как чесать правое ухо мизинцем левой ноги стоя на голове. Не?

rumgot ★★★★★ ()
Ответ на: комментарий от rumgot

А нахрена его делать публичным, если единственный способ использовать Foo снаружи - это передать его Baz? Ну, и допустим, будет он публичным, задачу сделать компактный интерфейс Baz это не решает никак, писать struct Foo f{«f»,1,2},g{«g»,2}, потом Baz(f,g) мне не кажется удобным. Структура Foo сама по себе никому не сдалась, это просто способ передать параметры.

JaM ()
#include <string>

struct Foo {
    std::string s;
    size_t b;
    size_t e;
};

template <typename... T>
auto Baz(Foo&& foo, T&&... t) {}

int main() {

    // Думаю, чтобы была возможна эта конструкция
    // (здесь тип второго аргумента должен быть выведен компилятором):
    Baz({"q", 10, 20}, {"g", 11, 50});

    // Должна быть возможна эта конструкция:
    auto t = {"q",10,20};
    // но эта конструкция сейчас невозможна, т.к. не описано в стандарте,
    // какой тут должен быть тип у переменной t
    // (по идее логично выводить это как tuple, возможно в будущем так и сделают)
}


rumgot ★★★★★ ()
Последнее исправление: rumgot (всего исправлений: 1)
Ответ на: комментарий от rumgot

Первая конструкция не работает по той же причине, что и вторая, невыводим тип {«g», 11, 50}. Либо явно указывать Baz, либо создавать функцию Baz(Foo&&,Foo&&). При увеличении количества аргументов повторить. Мне, кроме Baz(std::vector&&) и дополнительных фигурных скобок в вызове ничего толкового для общего случая в голову не приходит.

JaM ()
Ответ на: комментарий от JaM

Мне, кроме Baz(std::vector&&) и дополнительных фигурных скобок в вызове ничего толкового для общего случая в голову не приходит.

std::vector тяжеловат для этого. Может лучше использовать std::initializer_list?

rumgot ★★★★★ ()
Последнее исправление: rumgot (всего исправлений: 2)
Ответ на: комментарий от rumgot

А чем initializer_list будет легче в данном контексте? Вектор создаётся один раз, дальше только правой ссылкой передаётся, элементы вектора конструируются при его создании, вроде, никаких накладных расходов нет?

JaM ()
Ответ на: комментарий от JaM

Если ты вынес тип в публичный интерфейс функции, но сам тип в приватной секции класса - это плохое неинтуитивное поведение. Типа есть интерфейс публичный, но использовать его нельзя в определенных случаях.

rumgot ★★★★★ ()
Ответ на: комментарий от rumgot

Я не вынес тип в публичный интерфейс, я пытаюсь создать определённый публичный интерфейс, пользуясь типом как промежуточным звеном. Если бы можно было написать интерфейс без этого типа, я бы вообще его не вводил, он чисто технический, потому и спрятан.

JaM ()
Ответ на: комментарий от JaM

Например так использовать уже не получится:

s.Baz(Something::Foo{"q", 10, 20});

// или так:
Something::Foo f{"q", 10, 20};
s.Baz(std::move(f));

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

s.Baz({"q", 10, 20});
rumgot ★★★★★ ()
Последнее исправление: rumgot (всего исправлений: 2)
Ответ на: комментарий от JaM

Может тогда скопировать интерфейс QStringList?

Baz(Foos() << { "f",1,2 } << { "g",1,2 });

Подойдёт и другой лево-ассоциативный оператор. Можно от явного вызова Foos() избавиться:

Baz(Foo{ "f",1,2 } << { "g",1,2 });
xaizek ★★★★★ ()
Ответ на: комментарий от xaizek

А это идея. Можно заменить Baz на оператор, всё равно это функция-член класса. Сложности только с реализацией будут, мне надо все параметры обработать за раз, то есть оператор «<<» будет запоминать аргументы, а для их обработки придётся добавить что-то типа std::endl в конце.

JaM ()
Ответ на: комментарий от JaM

Ты не считаешь, что это программирование ради программирования?

Неужели это не проще и яснее:

Baz(Foo{ "f",1,2 }, Foo{ "g",1,2 });
rumgot ★★★★★ ()
Последнее исправление: rumgot (всего исправлений: 1)

Такое:

#include <array>
#include <string>
#include <iostream>

struct Foo
{
 std::string s;
 size_t b,e;
};

template<std::size_t sz>
auto Baz(std::array<Foo, sz> args)
{
	for (auto &a : args)
		std::cout << a.s << std::endl;
	return 0;
}

#define call_bas(sz, first, ...) \
Baz(std::array<Foo, sz>{ Foo first __VA_OPT__(,) __VA_ARGS__})


int main()
{
	call_bas(2, {"first", 4, 2}, {"second", 6, 2});
	return 0;
}

Правда от размер не удалось избавиться (а может полезно - передавать дефолтносозданные объекты?). Пробовал через std::experimental::make_array - не вышло.

Еще можно попробовать завернуть в tuple чтобы не плодить Foo.

pavlick ★★ ()
Последнее исправление: pavlick (всего исправлений: 1)
Ответ на: комментарий от pavlick

О, как интересно. Про variadic macros я не то, чтобы не знал, но в жизни бы не догадался посмотреть в их сторону.

P.S. Меня восхищает логика людей, придумавших variadic macros, и не создавших нормального способа подсчёта количества аргументов.

JaM ()
Ответ на: комментарий от JaM

ЦПП сила )

#include <array>
#include <string>
#include <iostream>
#include <tuple>

struct Foo
{
 std::string s;
 size_t b,e;
};

template<std::size_t sz>
auto Baz(std::array<Foo, sz> args)
{
	std::cout << "cnt = " <<  args.size() << std::endl;
	for (auto &a : args)
		std::cout << a.s << std::endl;
	return 0;
}

consteval int args_cnt(const char *str)
{
	int ret = 0;
	for (;  *str != '\0';  ++ str)
		if (*str == '}')
			++ ret;
	return ret;
}

#define call_baz(first, ...) \
Baz(std::array<Foo, args_cnt(#__VA_ARGS__)>{ Foo first __VA_OPT__(,) __VA_ARGS__})

int main()
{
	call_baz( {"first", 4, 2}, {"second", 6, 2}, {"third", 6, 2} );
	return 0;
}
pavlick ★★ ()
Последнее исправление: pavlick (всего исправлений: 2)
Ответ на: комментарий от rumgot

Использовать такое, конечно, не стоит, самому же потом читать это. Но решение, сочетающее сишные макросы с последним плюсовым стандартом, не лишено определённого очарования. Месье знает толк в извращениях )

JaM ()
Ответ на: комментарий от rumgot

Вы лучше гляньте как народ раньше кол-во аргументов считал ), более того - многие способы не подходят, тут сложный случай с запятыми в скобках, да и сам скобки. А новые фичи очень элегантно вписываются. Кстати, макрос сокращается до такого:

#define call_baz(...) \
  Baz(std::array<Foo, args_cnt(#__VA_ARGS__)>{ Foo __VA_ARGS__})

Ну не знаю, по-моему вполне симпатично, я на шаблонах писал в тысячу раз более лютый венегрет.

pavlick ★★ ()
Последнее исправление: pavlick (всего исправлений: 1)
Ответ на: комментарий от pavlick

Да уж сама привлекательность. Вот в библиотеках тоже кто-нибудь нахерачит так, а потом кому-то разбираться приходится с этими лабиринтами из макросов. И ведь многие по причине: а я вот хочу так писать.

rumgot ★★★★★ ()
Последнее исправление: rumgot (всего исправлений: 1)
Ответ на: комментарий от pavlick

А еще, работая в одной конторе на крупном комерческом проекте, видел как с помощью макросов делали объявление переменной некоторого типа:

some_type_decl(var_name)

Мне тут стало интересно и я посмотрел по комитам, кто этот гений, так выснилось - тимлид. Ну бля, ну тимлид.

rumgot ★★★★★ ()
Ответ на: комментарий от xaizek

Вспоминал я тут наше это обсуждение и вот к чему пришел. Если использовать сразу std::initializer_list - то будет конечно копирование, когда начнешь доставать значения из std::initializer_list и присваивать их далее. Но эй, ведь если будешь использовать функцию, принимающую аргумент вида std::vector<A>&& и будешь передавать список инициализации - то все равно будет использоваться конструктор std::vector(std::initializer_list<T>) т.е. при этом все равно будет сразу создаваться объект std::initializer_list с переданными значениями и далее уже из него эти значения будут скопированы в std::vector, т.е. один фиг копирование будет.

Вот иллюстрация написанного:

#include <iostream>
#include <string>
#include <vector>

using namespace std;

struct A {
    A() { cout << "A()" << endl; }
    A(const string& n) : name(n) { cout << "A(const string&)" << endl; }
    A(const A& a) : name(a.name) { cout << "A(const A&)" << endl; }
    A(A&& a) : name(move(a.name)) { cout << "A(A&&)" << endl; }

    ~A() { cout << "~A()" << endl; }

    A& operator=(const A& a) {
        cout << "operator=(const A&)" << endl;
        name = a.name;
        return *this;
    }

    A& operator=(A&& a) {
        cout << "operator=(A&&)" << endl;
        name = move(a.name);
        return *this;
    }

    string name;
};

void f(vector<A>&& args) {
    cout << "void f(vector<A>&& args)" << endl;
    // do something
}

// void f(initializer_list<A> args) {
//     cout << "void f(initializer_list<A> args)" << endl;
//     // do something
// }

int main() { f({{"first"}, {"second"}, {"third"}}); }

Вывод:

A(const string&)
A(const string&)
A(const string&)
A(const A&)
A(const A&)
A(const A&)
~A()
~A()
~A()
~A()
~A()
~A()

Таким образом все же использование std::vector получается избыточным и добавляет некоторые минимальные накладные расходы.

rumgot ★★★★★ ()
Последнее исправление: rumgot (всего исправлений: 1)
Ответ на: комментарий от rumgot

Но эй, ведь если будешь использовать функцию, принимающую аргумент вида std::vector<A>&& и будешь передавать список инициализации - то все равно будет использоваться конструктор std::vector(std::initializer_list<T>)

Блин, точно.

xaizek ★★★★★ ()