Thanks to Lincoln's comment below, I've changed this answer. 
The following answer properly handles 8-bit ints at compile time. It doees, however, require C++17. If you don't have C++17, you'll have to do something else (e.g. provide overloads of this function, one for uint8_t and one for int8_t, or use something besides "if constexpr", maybe enable_if). 
template< typename T >
std::string int_to_hex( T i )
{
    // Ensure this function is called with a template parameter that makes sense. Note: static_assert is only available in C++11 and higher.
    static_assert(std::is_integral<T>::value, "Template argument 'T' must be a fundamental integer type (e.g. int, short, etc..).");
    std::stringstream stream;
    stream << "0x" << std::setfill ('0') << std::setw(sizeof(T)*2) << std::hex;
    // If T is an 8-bit integer type (e.g. uint8_t or int8_t) it will be 
    // treated as an ASCII code, giving the wrong result. So we use C++17's
    // "if constexpr" to have the compiler decides at compile-time if it's 
    // converting an 8-bit int or not.
    if constexpr (std::is_same_v<std::uint8_t, T>)
    {        
        // Unsigned 8-bit unsigned int type. Cast to int (thanks Lincoln) to 
        // avoid ASCII code interpretation of the int. The number of hex digits 
        // in the  returned string will still be two, which is correct for 8 bits, 
        // because of the 'sizeof(T)' above.
        stream << static_cast<int>(i);
    }        
    else if (std::is_same_v<std::int8_t, T>)
    {
        // For 8-bit signed int, same as above, except we must first cast to unsigned 
        // int, because values above 127d (0x7f) in the int will cause further issues.
        // if we cast directly to int.
        stream << static_cast<int>(static_cast<uint8_t>(i));
    }
    else
    {
        // No cast needed for ints wider than 8 bits.
        stream << i;
    }
    return stream.str();
}
Original answer that doesn't handle 8-bit ints correctly as I thought it did:
Kornel Kisielewicz's answer is great. But a slight addition helps catch cases where you're calling this function with template arguments that don't make sense (e.g. float) or that would result in messy compiler errors (e.g. user-defined type).
template< typename T >
std::string int_to_hex( T i )
{
  // Ensure this function is called with a template parameter that makes sense. Note: static_assert is only available in C++11 and higher.
  static_assert(std::is_integral<T>::value, "Template argument 'T' must be a fundamental integer type (e.g. int, short, etc..).");
  std::stringstream stream;
  stream << "0x" 
         << std::setfill ('0') << std::setw(sizeof(T)*2) 
         << std::hex << i;
         // Optional: replace above line with this to handle 8-bit integers.
         // << std::hex << std::to_string(i);
  return stream.str();
}
I've edited this to add a call to std::to_string because 8-bit integer types (e.g. std::uint8_t values passed) to std::stringstream are treated as char, which doesn't give you the result you want. Passing such integers to std::to_string handles them correctly and doesn't hurt things when using other, larger integer types. Of course you may possibly suffer a slight performance hit in these cases since the std::to_string call is unnecessary. 
Note: I would have just added this in a comment to the original answer, but I don't have the rep to comment.