C++ different using declarations for different concepts

Viewed 108

Let's say, I have my List<T> class. I have a lot of functions where I have to pass a single object of my T type. For instance

void add(const T& item)
{
    ...
}

and it makes sense if T is some class or a struct. However, if T is a byte or integer, it's pointless or even wrong to pas it via reference, since memory pointer costs 8 bytes (4 on 32 bit system), i.e. I pass 1 byte size data type via 8 byte size pointer.

And so I decided to define argument data type using using directive. Kind of:

using argType = const T&; requires sizeof(T) > 8
using argType = T; requires sizeof(T) <= 8

But, obviously, this code doesn't work. Can you, please, propose me other solutions for that?

3 Answers

It sounds like what you need is conditional_t:

#include <type_traits>

template<class T>
class List {
  using argType = std::conditional_t<(sizeof(T) > 8), const T&, T>;
  
  void add(argType item) { }
};

To add to the answer provided here: https://stackoverflow.com/a/69864339/2963099 where conditional_t was suggested

It is important to make sure that items with non-trivial copy constructors are passed by reference, since a value copy can be very expensive. As such I would limit pass by value to trivially copyable objects.

Also using sizeof(T*) will work more generically than a hardcoded 8

Note that args are reversed and the test is to use pass by value, and objects with sizeof == 8 can still be pass by reference if needed.

using argType = std::conditional_t<
    (sizeof(T) <= sizeof(T *)) && std::is_trivially_copyable_v<T>,
    T, const T&>;

An example of usage:

#include <type_traits>
#include <vector>
#include <iostream>

struct Test
{
    Test() : x{0}
    {
        std::cout << "Default construct" << std::endl;
    }
    
    Test(const Test& rhs) : x{rhs.x}
    {
        std::cout << "copy: " << x << std::endl;
    }
    int * x;
};

template<class T>
struct List {
  using argType = std::conditional_t<(sizeof(T) <= sizeof(T *)) && std::is_trivially_copyable_v<T>, T, const T&>;
  
  static argType copy(argType item)
    { 
        std::cout << "Running Copy" << std::endl;
        return item;
    }
};

auto foo(typename List<Test>::argType t)
{
    return List<Test>::copy(t);
}

auto goo(typename List<unsigned long long>::argType t)
{
    return List<unsigned long long>::copy(t);
}

int main()
{
    Test t;
    std::cout << "Calling Test Copy" << std::endl;
    foo(t);
    std::cout << "After Test Copy" << std::endl;

    unsigned long long u;
    std::cout << "Calling ULL Copy" << std::endl;
    goo(u);
    std::cout << "After ULL Copy" << std::endl;
}

see it here: https://godbolt.org/z/WTKT8G6rh Note, you can see the parameter of foo() and goo() easily here

Note how Test would have been passed by value and incurred a possibly expensive copy, but checking for an expensive copy prevents this

With c++20 concepts you can add some constraints to your templates,

from en.cppreference.com/w/cpp/language/constraints

Class templates, function templates, and non-template functions (typically members of class templates) may be associated with a constraint, which specifies the requirements on template arguments, which can be used to select the most appropriate function overloads and template specializations. Named sets of such requirements are called concepts. Each concept is a predicate, evaluated at compile time, and becomes a part of the interface of a template where it is used as a constraint:

#include <string>
#include <cstddef>
#include <concepts>
 
// Declaration of the concept "Hashable", which is satisfied by any type 'T'
// such that for values 'a' of type 'T', the expression std::hash<T>{}(a)
// compiles and its result is convertible to std::size_t
template<typename T>
concept Hashable = requires(T a) {
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
 
struct meow {};
 
// Constrained C++20 function template:
template<Hashable T>
void f(T) {}
//
// Alternative ways to apply the same constraint:
// template<typename T>
//    requires Hashable<T>
// void f(T) {}
//
// template<typename T>
// void f(T) requires Hashable<T> {}
 
int main() {
  using std::operator""s;
  f("abc"s); // OK, std::string satisfies Hashable
//f(meow{}); // Error: meow does not satisfy Hashable
}

In your case I think you can constraint your template as follows:

template<typename T>
concept InfTo8Bytes = requires(T a) {
  sizeof (T) <= 8;
};

and use you concepts as follows:

template<InfTo8Bytes T>
class Foo {

};
Related