13

GMan has posted a code for the delicious auto_cast “operator” that allows to write code such as the following in C++:

float f = 4.0f;
int i = auto_cast(f);
// instead of:
int j = static_cast<int>(f);

or, more saliently,

T x = value;
typename nested_type<with, template_arguments>::type y = auto_cast(x);
// instead of
typedef typename nested_type<with, template_arguments>::type my_type;
my_type z = static_cast<my_type>(x);

Basically, the operator is great in removing unnecessary redundancy from a static_cast, and at the same time is still safe. It’s even more safe than static_cast since it prevents accidentally mismatching the types:

int i = 1234;
short s = static_cast<char>(i); // s == -46, not 1234!

However, j_random_hacker has noticed a flaw in the operator:

static_cast allows downcasts, which are potentially unsafe.

Indeed, an auto_cast should probably forbid downcasts because they may fail:

class base { };
class derived : public base { };

base b;
derived* pd = auto_cast(&b); // should fail at compile time.

Hence my question:

How would you modify the auto_cast implementation in order to forbid downcasts? This will probably involve enable_if. I’m especially interested in a solution that allows the compiler to provide good diagnostics in case of failure (= readable error messages).

GManNickG
  • 494,350
  • 52
  • 494
  • 543
Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • 2
    Note that `auto_cast` is not able to perform a downcast by reference semantics. It will always copy the source. That sounds safe to me: If the class you "downcast" to has no constructor accepting a base class object, it will simply fail to compile, just as if you used enable_if, but I imagine you would get even better errors, because if you use enable_if on the conversion function, you would get something like *"cannot convert auto_cast_wrapper to DerivedClass"*. If you leave it unchanged I think you may get *"Cannot static_cast Base& to DerivedClass"*. – Johannes Schaub - litb Apr 17 '11 at 12:19
  • @Johannes I was concerned with *pointers* to classes. `base b; derived* d = auto_cast(&b);` will unfortunately compile just fine. – Konrad Rudolph Apr 17 '11 at 12:41

3 Answers3

7

It appears you want to use the T{u} form of initialization.

template <typename U>
operator U()
{
    return U{std::forward<T>(mX)};
}

One of the reasons for these uniform initialization was that to use explicit constructors for creating a temporary, you need a cast aka T(u). With T{u} that problem was solved. For C++03, I imagine you could do something like this:

template<typename T>
struct construct_explicit {
  template<typename U>
  construct_explicit(U &u):t(u) { }
  template<typename U>
  construct_explicit(U const &u):t(u) { }

  T &get() { return t; }
  T const& get() const { return t; }

  T t;
};

Then you can say construct_explicit<U>(mX).get(), although in a case like in your conversion function, it also works to use a named variable as an intermediary step, I think

template <typename U>
operator U()
{
    // or C++03: U u(mX);
    U u(std::forward<T>(mX));
    return u;
}
Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • We have a winner. That solution seems perfect. Now `auto_cast` just needs to be renamed to `explicit_cast` (actually, I still prefer `auto_cast` …). – Konrad Rudolph Apr 17 '11 at 12:56
  • @Konrad: In what way is that cast explicit? :P – Xeo Apr 17 '11 at 12:59
  • @Xeo It’s explicit because we have to write it (as opposed to an implicit cast operator). The name would come from the fact that this code is using the `explicit` constructor of a class. – Konrad Rudolph Apr 17 '11 at 13:03
  • TBH, I don't quite understand how that solves the problem of downcasting as @Konrad states in his question. – Xeo Apr 17 '11 at 13:05
  • @Xeo Try compiling the downcast code example from my question … it won’t work any longer. – Konrad Rudolph Apr 17 '11 at 13:07
  • @Konrad: Ah, I see now how that solution works. :) Nice with relying on the language itself and not some TMP hackery. – Xeo Apr 17 '11 at 13:09
  • @Xeo `Derived *t(baseptr)` doesn't work. But `HasExplicitCtor c(usesExplicitCtor);` works fine. The `T{u}` form of initialization "roughly" is similar to constructing a temporary by `T t(u)` and then using `t` as the result of the temporary (there is a difference for c++0x, in that `T{u}` first considers initializer-list constructors, and that it works when `T` is an aggregate but `T t(u)` doesn't... But both will not allow non-implicit conversions). – Johannes Schaub - litb Apr 17 '11 at 13:11
  • In this way, perhaps using `T{u}` isn't really the way to go, because it will also make `struct A { int b; }; A a = auto_cast(1);` work. In that case, you can use `construct_explicit` or use a named variable initialized using parens. – Johannes Schaub - litb Apr 17 '11 at 13:13
  • Thanks for that explanation. I forgot for a moment that base pointer can't implicitly be casted to derived pointer. :) – Xeo Apr 17 '11 at 13:14
  • 1
    @Xeo it turns out that C++0x' `std::is_constructible` checks against this exact thing: Allow explicit constructors or explicit conversion functions, but forbid the casts like downcasts etc. – Johannes Schaub - litb Apr 17 '11 at 13:36
  • so `template>::type operator U()&& { return U(std::forward(t)); }` to explicitly and safely construct and avoid initializer lists? Still requires a copy/move constructor. `return {}` doesn't work, because it doesn't call explicit ctors. – Yakk - Adam Nevraumont Dec 10 '13 at 19:50
  • 1
    Frequently I need to assign a value of type `uint64_t` into a structure member that may be of type `uint32_t` or `uint64_t`, depending on the platform. I would prefer not to use `static_cast` since this adds unnecessary run-time overhead on the platforms where this structure member is of type `uint64_t`. In this situation `auto_cast` is almost perfect except that it emits a warning about the narrowing conversion. To solve this, I have [extended](https://github.com/whitslack/common/blob/b15cb66d2f41ed9/auto_cast.h) the solution given here to support warning-free narrowing conversions. – Matt Whitlock Mar 17 '16 at 11:23
3

You could use type-traits to disable the operator if T is a base of R. As we're in C++0x, you can explicitly static_assert(std::is_base_of<T, U>::value, "Cannot auto_cast downwards!");

Puppy
  • 144,682
  • 38
  • 256
  • 465
2

I wouldn't even use auto_cast because static_cast, const_cast, dynamic_cast and reinterpret_cast are also made ugly by design to help pointing code that could need refactoring : a ugly operation should have an ugly look.

A secondary reason for introducing the new-style cast was that C-style casts are very hard to spot in a program. For example, you can't conveniently search for casts using an ordinary editor or word processor. This near-invisibility of C-style casts is especially unfortunate because they are so potentially damaging. An ugly operation should have an ugly syntactic form. That observation was part of the reason for choosing the syntax for the new-style casts. A further reason was for the new-style casts to match the template notation, so that programmers can write their own casts, especially run-time checked casts.

http://www2.research.att.com/~bs/bs_faq2.html#static-cast

I prefer to see clearly in the code where it could be better, or where we explicitely need to make this ugly operation.

Klaim
  • 67,274
  • 36
  • 133
  • 188
  • You’re of course right in general. However I believe that the situations in which `auto_cast` would be used are (1) relatively safe and (2) a necessity. Casts in general should be avoided, true, but this isn’t always possible. – Konrad Rudolph Apr 17 '11 at 12:58
  • Yes but then they should be remarkable. auto_cast hides remarkable side of writing the other casts. – Klaim Apr 17 '11 at 13:20
  • What do you mean by “auto_cast hides remarkable side of writing the other casts”? FWIW I think that an `auto_cast` stands out sufficiently – compared to `static_cast` it merely removes the redundancy of having to input the target type *twice*, where once would suffice. So whether I write `T y = static_cast(x);` or `T y = auto_cast(x);` isn’t that important. The important thing is that I don’t write `T y = x;` or `T y = something(x)` where `something` does not imply that a cast is being made. The good thing about `auto_cast` is that the name clearly tells what’s happening: a cast. – Konrad Rudolph Apr 17 '11 at 13:27
  • So it's more meant to replace auto y = static_cast(x); ? Then yes it's useful if you don't have auto available. – Klaim Apr 17 '11 at 13:47