Введите 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);
и использовать ваше существующее тело для второго.
Мы получаем идеальную пересылку, затем перед копированием и выводом копируем.
Если вы хотите, чтобы мутат распространялся, вы, вероятно, ошибаетесь. Если вы настаиваете, подход @ Кассио выглядит прилично.