Введите traits, чтобы различать const char[N] и std::string?

Как мне написать шаблон переменной, который принимает как const char[N]s, так и std::strings в качестве параметров, но выполняет различное поведение в зависимости от типа параметра?

Пока что мой шаблон с вариадой выглядит следующим образом:

template<typename T>
void Capitalize_And_Output(T&& str) {
    std::transform(str.begin(), str.end(), str.begin(), ::toupper); //<- will not compile with char*s
    std::cout << str << std::endl;  
    return;
}

template<typename First, typename ... Strings>
void Capitalize_And_Output(First&& str, Strings&&... rest) {
    std::transform(str.begin(), str.end(), str.begin(), ::toupper); //<- will not compile with char*s
    std::cout << str << " ";
    Capitalize_And_Output(std::forward<Strings>(rest)...);
    return;
}

Используя "универсальные" ссылки, все принимается в функцию.
Однако, вызов такой функции не будет работать:

std::string hello = "hello";
std::string earth = "earth";

//fails because "planet" is a const char[N] and not a std::string
Capitalize_And_Output(hello,"planet","earth"); //outputs: "HELLO PLANET EARTH"

Это работает, если я сделаю следующее:

Capitalize_And_Output(hello,std::string("planet"),"earth"); //outputs: "HELLO PLANET EARTH"

But I don't want the user to be responsible for making this conversion. How can I pass that responsibility into the template function instead?

I have been trying to make the decision using type traits, but have not been successful. Я пытался использовать:

std::is_same<First, std::string&>::value   

but did not know how to make the branch decision. I do not believe this works inside of an if statement.

Maybe I need to use std::conditional somehow? Maybe I need to solve it by creating a local variable in the template that is type auto&&? I haven't had any success so far in the varying things I have tried.

4 ответа

Я вижу две проблемы с решением Simple:

(1) не удается скомпилировать этот тест

std::string hello = "hello";
const std::string earth = "earth";
Capitalize_And_Output(hello, "planet", earth);

так как earth это const std::string и нет перегрузки, которая может принять этот вызов. (Попытайся!)

(2) Не удается скомпилировать для типов (кроме const char* и тому подобное), которые могут быть преобразованы в std::string, например,

struct beautiful {
    operator std::string() const {
        return "beautiful";
    }
};

Capitalize_And_Output(hello, beautiful{}, "planet", earth);

Следующие реализации решают эти проблемы:

Новое решение: мое старое решение (ниже) работает, но оно неэффективно для char*, char[N], Кроме того, он сложен и использует некоторую хитрость разрешения перегрузки, чтобы избежать двусмысленности. Этот проще и эффективнее.

void Capitalize_And_Output_impl(const char* str) {
    while (char c = toupper(*str++))
        std::cout << c;
}

void Capitalize_And_Output_impl(std::string& str) {
    std::transform(str.begin(), str.end(), str.begin(), toupper);
    std::cout << str;
}

void Capitalize_And_Output_impl(const std::string& str) {
    Capitalize_And_Output_impl(str.data());
}

template<typename First>
void Capitalize_And_Output(First&& str) {
    Capitalize_And_Output_impl(std::forward<First>(str));
    std::cout << '\n';
}

template<typename First, typename ... Strings>
void Capitalize_And_Output(First&& str, Strings&&... rest) {
    Capitalize_And_Output_impl(std::forward<First>(str));
    std::cout << ' ';
    Capitalize_And_Output(std::forward<Strings>(rest)...);
}

Потому что я не использую std::transform (за исключением второй перегрузки), ему не нужно заранее знать размер строки. Поэтому для char* нет необходимости звонить std::strlen (как и в других решениях).

Небольшая деталь, на которую следует обратить внимание, это то, что эта реализация печатает только пробелы между словами. (Он не печатает один после последнего слова.)

Старое решение:

void Capitalize_And_Output_impl(std::string& str, int) {
    std::transform(str.begin(), str.end(), str.begin(), ::toupper);
    std::cout << str << ' ';
}

void Capitalize_And_Output_impl(std::string str, long) {
    Capitalize_And_Output_impl(str, 0);
}

void Capitalize_And_Output() {
    std::cout << '\n';
}

template<typename First, typename ... Strings>
void Capitalize_And_Output(First&& str, Strings&&... rest) {
    Capitalize_And_Output_impl(std::forward<First>(str), 0);
    Capitalize_And_Output(std::forward<Strings>(rest)...);
}

Я думаю, что два Capitalize_And_Output_impl перегрузки заслуживают объяснений.

Сначала рассмотрим второй аргумент (int/long). Первая перегрузка может принимать не const l Значения, которые пишутся с заглавной буквы при выходе (в соответствии с просьбой Тревора Хикни в комментарии к решению Simple).

Второе oveload предназначено, чтобы взять все остальное, т.е. const lvalues. Идея состоит в том, чтобы скопировать аргумент в lvalue, который затем передается первой перегрузке. Эта функция, естественно, может быть реализована таким образом (все еще не учитывая второй аргумент):

void Capitalize_And_Output_impl(const std::string& str) {
    std::string tmp(str);
    Capitalize_And_Output_impl(tmp);
}

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

void Capitalize_And_Output_impl(std::string str) {
    Capitalize_And_Output_impl(str);
}

К сожалению, что касается первой перегрузки, звонки Capitalize_And_Output_impl Значения l также могут быть направлены на эту перегрузку. Это приводит к неоднозначности, на которую жалуется компилятор. Вот почему нам нужен второй аргумент.

Первая перегрузка занимает int а второй занимает long, Поэтому прохождение буквального 0, который является int, делает первую перегрузку предпочтительнее второй, но только при возникновении неоднозначности. В других случаях, т. Е. Когда первый аргумент является rvalue или const lvalue первая перегрузка не может использоваться, тогда как вторая может после литерала 0 повышен до long,

Два последних замечания. (1) если вы хотите избежать рекурсивного вызова в Capitalize_And_Output (Я думаю, это просто вопрос вкуса), тогда вы можете использовать тот же обман, что и в решении Simple (через unpack) и (2) я не вижу необходимости проходить лямбда-обертывание ::toupper как в решении Simple.

Вам не нужны черты типа для этого:

char safer_toupper(unsigned char const c)
{
    return static_cast<char>(std::toupper(c));
}

void Capitalize_And_Output_Impl(std::string& str)
{
    auto const first = str.begin();
    std::transform(first, str.end(), first, safer_toupper);
    std::cout << str;
}

void Capitalize_And_Output_Impl(std::string const& str)
{
    std::transform(str.begin(), str.end(),
                   std::ostreambuf_iterator<char>(std::cout),
                   safer_toupper);
}

void Capitalize_And_Output_Impl(char const* const str)
{
    std::transform(str, str + std::strlen(str),
                   std::ostreambuf_iterator<char>(std::cout),
                   safer_toupper);
}

template<typename... Strings>
void Capitalize_And_Output(Strings&&... rest)
{
    int const unpack[]{0, (Capitalize_And_Output_Impl(rest),
                           std::cout << ' ', 0)...};
    static_cast<void>(unpack);
    std::cout << std::endl;
}

Эта версия не делает ненужного копирования аргументов, не вводит ненужных временных строк и избегает вызова strlen() для буквенных строк, длина которых известна во время компиляции.

#include <algorithm>
#include <cctype>
#include <cstring>
#include <iostream>
#include <iterator>
#include <string>
#include <type_traits>
#include <vector>

template<typename I> void CapitalizeAndOutputImpl(I first, I last) {
    std::string t;
    std::transform(first, last, std::back_inserter(t), std::toupper);
    std::cout << t << " ";
}

template<typename T>
struct CapitalizeAndOutputHelper {
    void operator()(const T& s) {
        CapitalizeAndOutputImpl(std::begin(s), std::end(s));
    }
};

template<typename T>
struct CapitalizeAndOutputHelper<T*> {
    void operator()(const T* s) {
        CapitalizeAndOutputImpl(s, s + std::strlen(s));
    }
};

template<typename T> void CapitalizeAndOutput(T&& s) {
    CapitalizeAndOutputHelper<std::remove_reference<T>::type>()(s);
    std::cout << std::endl;
}

template<typename First, typename... Rest> void CapitalizeAndOutput(First&& first, Rest&&... rest) {
    CapitalizeAndOutputHelper<std::remove_reference<First>::type>()(first);
    CapitalizeAndOutput(rest...);
}

int main() {
    std::string hello{ "string hello" };
    const std::string world{ "const string world" };
    char arrHello[] = "char[] hello";
    const char vHelloInit[] = "char* hello";
    std::vector<char> vHello(std::begin(vHelloInit), std::end(vHelloInit));
    const char* cworld = "const char* world";
    CapitalizeAndOutput(hello, world, arrHello, "literal world", vHello.data(), cworld);
}

Безусловно, проще всего иметь две перегрузки:

void do_stuff() {}
template<class...Ts>
void do_stuff(std::string s, Ts&&... ts);

и использовать ваше существующее тело для второго.

Мы получаем идеальную пересылку, затем перед копированием и выводом копируем.

Если вы хотите, чтобы мутат распространялся, вы, вероятно, ошибаетесь. Если вы настаиваете, подход @ Кассио выглядит прилично.

Другие вопросы по тегам